@kolatts/pncli 1.1.0 → 1.3.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/README.md +13 -12
- package/copilot-instructions.md +238 -5
- package/dist/cli.js +2700 -127
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
|
|
7
7
|
// src/lib/output.ts
|
|
8
|
+
import fs from "fs";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
|
|
10
11
|
// src/lib/errors.ts
|
|
@@ -19,6 +20,24 @@ var PncliError = class extends Error {
|
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
// src/lib/exitCodes.ts
|
|
24
|
+
var ExitCode = {
|
|
25
|
+
SUCCESS: 0,
|
|
26
|
+
GENERAL_ERROR: 1,
|
|
27
|
+
USAGE_ERROR: 2,
|
|
28
|
+
NETWORK_ERROR: 69,
|
|
29
|
+
// EX_UNAVAILABLE from sysexits.h
|
|
30
|
+
AUTH_ERROR: 77,
|
|
31
|
+
// EX_NOPERM from sysexits.h
|
|
32
|
+
CONFIG_ERROR: 78
|
|
33
|
+
// EX_CONFIG from sysexits.h
|
|
34
|
+
};
|
|
35
|
+
function exitCodeFromStatus(httpStatus) {
|
|
36
|
+
if (httpStatus === 401 || httpStatus === 403) return ExitCode.AUTH_ERROR;
|
|
37
|
+
if (httpStatus === 0) return ExitCode.NETWORK_ERROR;
|
|
38
|
+
return ExitCode.GENERAL_ERROR;
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
// src/lib/output.ts
|
|
23
42
|
var globalOptions = { pretty: false, verbose: false };
|
|
24
43
|
var globalUser = { email: void 0, userId: void 0 };
|
|
@@ -60,10 +79,15 @@ function fail(err, service, action, startTime) {
|
|
|
60
79
|
};
|
|
61
80
|
const msg = globalOptions.pretty ? chalk.red("\u2717 Error: ") + errorDetail.message : null;
|
|
62
81
|
if (msg) process.stderr.write(msg + "\n");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
const output = (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + "\n";
|
|
83
|
+
const exitCode = err instanceof PncliError ? exitCodeFromStatus(err.status) : ExitCode.GENERAL_ERROR;
|
|
84
|
+
try {
|
|
85
|
+
fs.writeSync(process.stdout.fd, output);
|
|
86
|
+
} catch (writeErr) {
|
|
87
|
+
if (writeErr.code !== "EPIPE") throw writeErr;
|
|
88
|
+
}
|
|
89
|
+
process.exitCode = exitCode;
|
|
90
|
+
throw new PncliError(errorDetail.message, errorDetail.status);
|
|
67
91
|
}
|
|
68
92
|
function log(message) {
|
|
69
93
|
if (globalOptions.verbose) {
|
|
@@ -79,7 +103,7 @@ function warn(message) {
|
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
// src/lib/config.ts
|
|
82
|
-
import
|
|
106
|
+
import fs2 from "fs";
|
|
83
107
|
import os from "os";
|
|
84
108
|
import path from "path";
|
|
85
109
|
import { execSync } from "child_process";
|
|
@@ -90,6 +114,17 @@ var ENV_KEYS = {
|
|
|
90
114
|
JIRA_API_TOKEN: "PNCLI_JIRA_API_TOKEN",
|
|
91
115
|
BITBUCKET_BASE_URL: "PNCLI_BITBUCKET_BASE_URL",
|
|
92
116
|
BITBUCKET_PAT: "PNCLI_BITBUCKET_PAT",
|
|
117
|
+
CONFLUENCE_BASE_URL: "PNCLI_CONFLUENCE_BASE_URL",
|
|
118
|
+
CONFLUENCE_API_TOKEN: "PNCLI_CONFLUENCE_API_TOKEN",
|
|
119
|
+
ARTIFACTORY_BASE_URL: "PNCLI_ARTIFACTORY_BASE_URL",
|
|
120
|
+
ARTIFACTORY_TOKEN: "PNCLI_ARTIFACTORY_TOKEN",
|
|
121
|
+
ARTIFACTORY_REPO_NPM: "PNCLI_ARTIFACTORY_REPO_NPM",
|
|
122
|
+
ARTIFACTORY_REPO_NUGET: "PNCLI_ARTIFACTORY_REPO_NUGET",
|
|
123
|
+
ARTIFACTORY_REPO_MAVEN: "PNCLI_ARTIFACTORY_REPO_MAVEN",
|
|
124
|
+
SONAR_BASE_URL: "PNCLI_SONAR_BASE_URL",
|
|
125
|
+
SONAR_TOKEN: "PNCLI_SONAR_TOKEN",
|
|
126
|
+
SDE_BASE_URL: "PNCLI_SDE_BASE_URL",
|
|
127
|
+
SDE_TOKEN: "PNCLI_SDE_TOKEN",
|
|
93
128
|
CONFIG_PATH: "PNCLI_CONFIG_PATH"
|
|
94
129
|
};
|
|
95
130
|
function getGlobalConfigPath(overridePath) {
|
|
@@ -100,7 +135,7 @@ function getGlobalConfigPath(overridePath) {
|
|
|
100
135
|
}
|
|
101
136
|
function loadJsonFile(filePath) {
|
|
102
137
|
try {
|
|
103
|
-
const content =
|
|
138
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
104
139
|
return JSON.parse(content);
|
|
105
140
|
} catch {
|
|
106
141
|
return null;
|
|
@@ -122,7 +157,9 @@ function mergeCustomFields(global, repo) {
|
|
|
122
157
|
function mergeDefaults(global, repo) {
|
|
123
158
|
return {
|
|
124
159
|
jira: { ...global?.jira, ...repo?.jira },
|
|
125
|
-
bitbucket: { ...global?.bitbucket, ...repo?.bitbucket }
|
|
160
|
+
bitbucket: { ...global?.bitbucket, ...repo?.bitbucket },
|
|
161
|
+
sonar: { ...global?.sonar, ...repo?.sonar },
|
|
162
|
+
sde: { ...global?.sde, ...repo?.sde }
|
|
126
163
|
};
|
|
127
164
|
}
|
|
128
165
|
function loadConfig(opts = {}) {
|
|
@@ -148,21 +185,40 @@ function loadConfig(opts = {}) {
|
|
|
148
185
|
baseUrl: process.env[ENV_KEYS.BITBUCKET_BASE_URL] ?? globalConfig.bitbucket?.baseUrl,
|
|
149
186
|
pat: process.env[ENV_KEYS.BITBUCKET_PAT] ?? globalConfig.bitbucket?.pat
|
|
150
187
|
},
|
|
188
|
+
confluence: {
|
|
189
|
+
baseUrl: process.env[ENV_KEYS.CONFLUENCE_BASE_URL] ?? globalConfig.confluence?.baseUrl,
|
|
190
|
+
apiToken: process.env[ENV_KEYS.CONFLUENCE_API_TOKEN] ?? globalConfig.confluence?.apiToken ?? process.env[ENV_KEYS.JIRA_API_TOKEN] ?? globalConfig.jira?.apiToken
|
|
191
|
+
},
|
|
192
|
+
artifactory: {
|
|
193
|
+
baseUrl: process.env[ENV_KEYS.ARTIFACTORY_BASE_URL] ?? globalConfig.artifactory?.baseUrl,
|
|
194
|
+
token: process.env[ENV_KEYS.ARTIFACTORY_TOKEN] ?? globalConfig.artifactory?.token,
|
|
195
|
+
npmRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NPM] ?? globalConfig.artifactory?.npmRepo,
|
|
196
|
+
nugetRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_NUGET] ?? globalConfig.artifactory?.nugetRepo,
|
|
197
|
+
mavenRepo: process.env[ENV_KEYS.ARTIFACTORY_REPO_MAVEN] ?? globalConfig.artifactory?.mavenRepo
|
|
198
|
+
},
|
|
199
|
+
sonar: {
|
|
200
|
+
baseUrl: process.env[ENV_KEYS.SONAR_BASE_URL] ?? globalConfig.sonar?.baseUrl,
|
|
201
|
+
token: process.env[ENV_KEYS.SONAR_TOKEN] ?? globalConfig.sonar?.token
|
|
202
|
+
},
|
|
203
|
+
sde: {
|
|
204
|
+
baseUrl: process.env[ENV_KEYS.SDE_BASE_URL] ?? globalConfig.sde?.baseUrl,
|
|
205
|
+
token: process.env[ENV_KEYS.SDE_TOKEN] ?? globalConfig.sde?.token
|
|
206
|
+
},
|
|
151
207
|
defaults: mergedDefaults
|
|
152
208
|
};
|
|
153
209
|
}
|
|
154
210
|
function writeGlobalConfig(config, configPath) {
|
|
155
211
|
const filePath = getGlobalConfigPath(configPath);
|
|
156
212
|
const dir = path.dirname(filePath);
|
|
157
|
-
if (!
|
|
158
|
-
|
|
213
|
+
if (!fs2.existsSync(dir)) {
|
|
214
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
159
215
|
}
|
|
160
|
-
|
|
216
|
+
fs2.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
161
217
|
}
|
|
162
218
|
function writeRepoConfig(config) {
|
|
163
219
|
const repoRoot = getRepoRoot();
|
|
164
220
|
const targetDir = repoRoot ?? process.cwd();
|
|
165
|
-
|
|
221
|
+
fs2.writeFileSync(path.join(targetDir, ".pncli.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
166
222
|
}
|
|
167
223
|
function setConfigValue(key, value, configPath) {
|
|
168
224
|
const filePath = getGlobalConfigPath(configPath);
|
|
@@ -190,6 +246,22 @@ function maskConfig(config) {
|
|
|
190
246
|
bitbucket: {
|
|
191
247
|
...config.bitbucket,
|
|
192
248
|
pat: config.bitbucket.pat ? "***" : void 0
|
|
249
|
+
},
|
|
250
|
+
confluence: {
|
|
251
|
+
...config.confluence,
|
|
252
|
+
apiToken: config.confluence.apiToken ? "***" : void 0
|
|
253
|
+
},
|
|
254
|
+
artifactory: {
|
|
255
|
+
...config.artifactory,
|
|
256
|
+
token: config.artifactory.token ? "***" : void 0
|
|
257
|
+
},
|
|
258
|
+
sonar: {
|
|
259
|
+
...config.sonar,
|
|
260
|
+
token: config.sonar.token ? "***" : void 0
|
|
261
|
+
},
|
|
262
|
+
sde: {
|
|
263
|
+
...config.sde,
|
|
264
|
+
token: config.sde.token ? "***" : void 0
|
|
193
265
|
}
|
|
194
266
|
};
|
|
195
267
|
}
|
|
@@ -394,8 +466,9 @@ function getGitContext(config) {
|
|
|
394
466
|
}
|
|
395
467
|
|
|
396
468
|
// src/lib/http.ts
|
|
397
|
-
|
|
398
|
-
|
|
469
|
+
import fs3 from "fs";
|
|
470
|
+
function buildUrl(base, path6, params) {
|
|
471
|
+
const url = new URL(path6, base.endsWith("/") ? base : base + "/");
|
|
399
472
|
if (params) {
|
|
400
473
|
for (const [key, val] of Object.entries(params)) {
|
|
401
474
|
if (val !== void 0) {
|
|
@@ -440,8 +513,25 @@ async function request(url, init, timeoutMs) {
|
|
|
440
513
|
try {
|
|
441
514
|
const body = await response.text();
|
|
442
515
|
const parsed = JSON.parse(body);
|
|
443
|
-
|
|
444
|
-
|
|
516
|
+
const parts = [];
|
|
517
|
+
if (Array.isArray(parsed.errorMessages)) {
|
|
518
|
+
parts.push(...parsed.errorMessages.filter(Boolean));
|
|
519
|
+
}
|
|
520
|
+
if (parsed.errors && typeof parsed.errors === "object" && !Array.isArray(parsed.errors)) {
|
|
521
|
+
for (const [field, msg] of Object.entries(parsed.errors)) {
|
|
522
|
+
parts.push(`${field}: ${msg}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (Array.isArray(parsed.errors)) {
|
|
526
|
+
for (const e of parsed.errors) {
|
|
527
|
+
if (e?.message) parts.push(e.message);
|
|
528
|
+
else if (e?.msg) parts.push(e.msg);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (parts.length === 0 && parsed.message) {
|
|
532
|
+
parts.push(String(parsed.message));
|
|
533
|
+
}
|
|
534
|
+
if (parts.length > 0) message = parts.join("; ");
|
|
445
535
|
} catch {
|
|
446
536
|
}
|
|
447
537
|
throw new PncliError(message, response.status, url);
|
|
@@ -449,7 +539,9 @@ async function request(url, init, timeoutMs) {
|
|
|
449
539
|
if (response.status === 204) {
|
|
450
540
|
return void 0;
|
|
451
541
|
}
|
|
452
|
-
|
|
542
|
+
const text = await response.text();
|
|
543
|
+
if (!text) return void 0;
|
|
544
|
+
return JSON.parse(text);
|
|
453
545
|
}
|
|
454
546
|
throw lastError ?? new PncliError("Request failed after retries", 1, url);
|
|
455
547
|
}
|
|
@@ -466,7 +558,8 @@ var HttpClient = class {
|
|
|
466
558
|
return {
|
|
467
559
|
"Authorization": `Bearer ${apiToken}`,
|
|
468
560
|
"Content-Type": "application/json",
|
|
469
|
-
"Accept": "application/json"
|
|
561
|
+
"Accept": "application/json",
|
|
562
|
+
"Connection": "close"
|
|
470
563
|
};
|
|
471
564
|
}
|
|
472
565
|
bitbucketHeaders() {
|
|
@@ -475,13 +568,14 @@ var HttpClient = class {
|
|
|
475
568
|
return {
|
|
476
569
|
"Authorization": `Bearer ${pat}`,
|
|
477
570
|
"Content-Type": "application/json",
|
|
478
|
-
"Accept": "application/json"
|
|
571
|
+
"Accept": "application/json",
|
|
572
|
+
"Connection": "close"
|
|
479
573
|
};
|
|
480
574
|
}
|
|
481
|
-
async jira(
|
|
575
|
+
async jira(path6, opts = {}) {
|
|
482
576
|
const baseUrl = this.config.jira.baseUrl;
|
|
483
577
|
if (!baseUrl) throw new PncliError("Jira baseUrl not configured. Run: pncli config init");
|
|
484
|
-
const url = buildUrl(baseUrl,
|
|
578
|
+
const url = buildUrl(baseUrl, path6, opts.params);
|
|
485
579
|
const headers = this.jiraHeaders();
|
|
486
580
|
const init = {
|
|
487
581
|
method: opts.method ?? "GET",
|
|
@@ -490,19 +584,20 @@ var HttpClient = class {
|
|
|
490
584
|
};
|
|
491
585
|
if (this.dryRun) {
|
|
492
586
|
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
493
|
-
|
|
587
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
494
588
|
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
495
|
-
`)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
process.
|
|
589
|
+
` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
|
|
590
|
+
` : "");
|
|
591
|
+
fs3.writeSync(process.stderr.fd, msg);
|
|
592
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
593
|
+
throw new PncliError("dry-run", 0);
|
|
499
594
|
}
|
|
500
595
|
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
501
596
|
}
|
|
502
|
-
async bitbucket(
|
|
597
|
+
async bitbucket(path6, opts = {}) {
|
|
503
598
|
const baseUrl = this.config.bitbucket.baseUrl;
|
|
504
599
|
if (!baseUrl) throw new PncliError("Bitbucket baseUrl not configured. Run: pncli config init");
|
|
505
|
-
const url = buildUrl(baseUrl,
|
|
600
|
+
const url = buildUrl(baseUrl, path6, opts.params);
|
|
506
601
|
const headers = this.bitbucketHeaders();
|
|
507
602
|
const init = {
|
|
508
603
|
method: opts.method ?? "GET",
|
|
@@ -511,15 +606,148 @@ Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
|
511
606
|
};
|
|
512
607
|
if (this.dryRun) {
|
|
513
608
|
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
514
|
-
|
|
609
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
515
610
|
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
516
|
-
`)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
process.
|
|
611
|
+
` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
|
|
612
|
+
` : "");
|
|
613
|
+
fs3.writeSync(process.stderr.fd, msg);
|
|
614
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
615
|
+
throw new PncliError("dry-run", 0);
|
|
616
|
+
}
|
|
617
|
+
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
618
|
+
}
|
|
619
|
+
confluenceHeaders() {
|
|
620
|
+
const { apiToken } = this.config.confluence;
|
|
621
|
+
if (!apiToken) throw new PncliError("Confluence credentials not configured. Run: pncli config init");
|
|
622
|
+
return {
|
|
623
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
624
|
+
"Content-Type": "application/json",
|
|
625
|
+
"Accept": "application/json",
|
|
626
|
+
"Connection": "close"
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
async confluence(path6, opts = {}) {
|
|
630
|
+
const baseUrl = this.config.confluence.baseUrl;
|
|
631
|
+
if (!baseUrl) throw new PncliError("Confluence baseUrl not configured. Run: pncli config init");
|
|
632
|
+
const url = buildUrl(baseUrl, path6, opts.params);
|
|
633
|
+
const headers = this.confluenceHeaders();
|
|
634
|
+
const init = {
|
|
635
|
+
method: opts.method ?? "GET",
|
|
636
|
+
headers,
|
|
637
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
638
|
+
};
|
|
639
|
+
if (this.dryRun) {
|
|
640
|
+
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
641
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
642
|
+
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
643
|
+
` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
|
|
644
|
+
` : "");
|
|
645
|
+
fs3.writeSync(process.stderr.fd, msg);
|
|
646
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
647
|
+
throw new PncliError("dry-run", 0);
|
|
648
|
+
}
|
|
649
|
+
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
650
|
+
}
|
|
651
|
+
sdeHeaders() {
|
|
652
|
+
const { token } = this.config.sde;
|
|
653
|
+
if (!token) throw new PncliError("SDElements credentials not configured. Run: pncli config init");
|
|
654
|
+
return {
|
|
655
|
+
"Authorization": `Token ${token}`,
|
|
656
|
+
"Content-Type": "application/json",
|
|
657
|
+
"Accept": "application/json",
|
|
658
|
+
"Connection": "close"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async sde(path6, opts = {}) {
|
|
662
|
+
const baseUrl = this.config.sde.baseUrl;
|
|
663
|
+
if (!baseUrl) throw new PncliError("SDElements baseUrl not configured. Run: pncli config init");
|
|
664
|
+
const url = buildUrl(baseUrl, path6, opts.params);
|
|
665
|
+
const headers = this.sdeHeaders();
|
|
666
|
+
const init = {
|
|
667
|
+
method: opts.method ?? "GET",
|
|
668
|
+
headers,
|
|
669
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
670
|
+
};
|
|
671
|
+
if (this.dryRun) {
|
|
672
|
+
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
673
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
674
|
+
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
675
|
+
` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
|
|
676
|
+
` : "");
|
|
677
|
+
fs3.writeSync(process.stderr.fd, msg);
|
|
678
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
679
|
+
throw new PncliError("dry-run", 0);
|
|
680
|
+
}
|
|
681
|
+
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
682
|
+
}
|
|
683
|
+
async sdePaginate(fetchPage) {
|
|
684
|
+
const results = [];
|
|
685
|
+
let page = 1;
|
|
686
|
+
const pageSize = 100;
|
|
687
|
+
while (true) {
|
|
688
|
+
const response = await fetchPage(page, pageSize);
|
|
689
|
+
results.push(...response.results);
|
|
690
|
+
if (results.length >= response.count || response.results.length === 0) break;
|
|
691
|
+
page++;
|
|
692
|
+
}
|
|
693
|
+
return results;
|
|
694
|
+
}
|
|
695
|
+
sonarHeaders() {
|
|
696
|
+
const { token } = this.config.sonar;
|
|
697
|
+
if (!token) throw new PncliError("SonarQube credentials not configured. Run: pncli config init");
|
|
698
|
+
return {
|
|
699
|
+
"Authorization": `Bearer ${token}`,
|
|
700
|
+
"Content-Type": "application/json",
|
|
701
|
+
"Accept": "application/json",
|
|
702
|
+
"Connection": "close"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async sonar(path6, opts = {}) {
|
|
706
|
+
const baseUrl = this.config.sonar.baseUrl;
|
|
707
|
+
if (!baseUrl) throw new PncliError("SonarQube baseUrl not configured. Run: pncli config init");
|
|
708
|
+
const url = buildUrl(baseUrl, path6, opts.params);
|
|
709
|
+
const headers = this.sonarHeaders();
|
|
710
|
+
const init = {
|
|
711
|
+
method: opts.method ?? "GET",
|
|
712
|
+
headers,
|
|
713
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
714
|
+
};
|
|
715
|
+
if (this.dryRun) {
|
|
716
|
+
const safeHeaders = { ...headers, Authorization: "[REDACTED]" };
|
|
717
|
+
const msg = `DRY RUN: ${init.method} ${url}
|
|
718
|
+
Headers: ${JSON.stringify(safeHeaders, null, 2)}
|
|
719
|
+
` + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}
|
|
720
|
+
` : "");
|
|
721
|
+
fs3.writeSync(process.stderr.fd, msg);
|
|
722
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
723
|
+
throw new PncliError("dry-run", 0);
|
|
520
724
|
}
|
|
521
725
|
return request(url, init, opts.timeoutMs ?? 3e4);
|
|
522
726
|
}
|
|
727
|
+
async sonarPaginate(fetchPage) {
|
|
728
|
+
const results = [];
|
|
729
|
+
let page = 1;
|
|
730
|
+
const pageSize = 500;
|
|
731
|
+
while (true) {
|
|
732
|
+
const response = await fetchPage(page, pageSize);
|
|
733
|
+
results.push(...response.items);
|
|
734
|
+
if (results.length >= response.total || response.items.length === 0) break;
|
|
735
|
+
page++;
|
|
736
|
+
}
|
|
737
|
+
return results;
|
|
738
|
+
}
|
|
739
|
+
async confluencePaginate(fetchPage) {
|
|
740
|
+
const results = [];
|
|
741
|
+
let start = 0;
|
|
742
|
+
const limit = 25;
|
|
743
|
+
while (true) {
|
|
744
|
+
const page = await fetchPage(start, limit);
|
|
745
|
+
results.push(...page.results);
|
|
746
|
+
if (!page._links.next) break;
|
|
747
|
+
start += page.size;
|
|
748
|
+
}
|
|
749
|
+
return results;
|
|
750
|
+
}
|
|
523
751
|
async paginate(fetchPage) {
|
|
524
752
|
const results = [];
|
|
525
753
|
let start = 0;
|
|
@@ -1412,124 +1640,2423 @@ function registerBitbucketCommands(program2) {
|
|
|
1412
1640
|
});
|
|
1413
1641
|
}
|
|
1414
1642
|
|
|
1643
|
+
// src/services/confluence/client.ts
|
|
1644
|
+
var API3 = "/rest/api";
|
|
1645
|
+
var ConfluenceClient = class {
|
|
1646
|
+
constructor(http) {
|
|
1647
|
+
this.http = http;
|
|
1648
|
+
}
|
|
1649
|
+
http;
|
|
1650
|
+
async getPage(id, expand = "body.storage,version,space,ancestors") {
|
|
1651
|
+
return this.http.confluence(`${API3}/content/${id}`, {
|
|
1652
|
+
params: { expand }
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
async getPageByTitle(spaceKey, title) {
|
|
1656
|
+
const result = await this.http.confluence(`${API3}/content`, {
|
|
1657
|
+
params: { spaceKey, title, expand: "body.storage,version,space", type: "page" }
|
|
1658
|
+
});
|
|
1659
|
+
return result.results[0] ?? null;
|
|
1660
|
+
}
|
|
1661
|
+
async listPages(spaceKey, opts = {}) {
|
|
1662
|
+
const initialStart = opts.start ?? 0;
|
|
1663
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1664
|
+
return this.http.confluence(`${API3}/content`, {
|
|
1665
|
+
params: { spaceKey, type: "page", expand: "version", start: initialStart + start, limit: opts.limit ?? limit }
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
async getPageChildren(id) {
|
|
1670
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1671
|
+
return this.http.confluence(`${API3}/content/${id}/child/page`, {
|
|
1672
|
+
params: { expand: "version", start, limit }
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
async getLabels(id) {
|
|
1677
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1678
|
+
return this.http.confluence(`${API3}/content/${id}/label`, {
|
|
1679
|
+
params: { start, limit }
|
|
1680
|
+
});
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
async search(cql, opts = {}) {
|
|
1684
|
+
return this.http.confluence(`${API3}/content/search`, {
|
|
1685
|
+
params: {
|
|
1686
|
+
cql,
|
|
1687
|
+
start: opts.start ?? 0,
|
|
1688
|
+
limit: opts.limit ?? 25,
|
|
1689
|
+
...opts.expand ? { expand: opts.expand } : {}
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
async createPage(opts) {
|
|
1694
|
+
const body = {
|
|
1695
|
+
type: "page",
|
|
1696
|
+
title: opts.title,
|
|
1697
|
+
space: { key: opts.spaceKey },
|
|
1698
|
+
ancestors: opts.parentId ? [{ id: opts.parentId }] : [],
|
|
1699
|
+
body: {
|
|
1700
|
+
storage: {
|
|
1701
|
+
value: opts.body,
|
|
1702
|
+
representation: opts.representation ?? "storage"
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
return this.http.confluence(`${API3}/content`, {
|
|
1707
|
+
method: "POST",
|
|
1708
|
+
body
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
async updatePage(id, opts) {
|
|
1712
|
+
const body = {
|
|
1713
|
+
version: { number: opts.version },
|
|
1714
|
+
type: "page",
|
|
1715
|
+
title: opts.title,
|
|
1716
|
+
status: opts.status ?? "current"
|
|
1717
|
+
};
|
|
1718
|
+
if (opts.body !== void 0) {
|
|
1719
|
+
body.body = {
|
|
1720
|
+
storage: {
|
|
1721
|
+
value: opts.body,
|
|
1722
|
+
representation: opts.representation ?? "storage"
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
return this.http.confluence(`${API3}/content/${id}`, {
|
|
1727
|
+
method: "PUT",
|
|
1728
|
+
body
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
async deletePage(id) {
|
|
1732
|
+
return this.http.confluence(`${API3}/content/${id}`, { method: "DELETE" });
|
|
1733
|
+
}
|
|
1734
|
+
async listComments(pageId) {
|
|
1735
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1736
|
+
return this.http.confluence(`${API3}/content/${pageId}/child/comment`, {
|
|
1737
|
+
params: { expand: "body.storage,version", start, limit }
|
|
1738
|
+
});
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
async addComment(pageId, body, representation = "storage") {
|
|
1742
|
+
return this.http.confluence(`${API3}/content`, {
|
|
1743
|
+
method: "POST",
|
|
1744
|
+
body: {
|
|
1745
|
+
type: "comment",
|
|
1746
|
+
container: { id: pageId, type: "page" },
|
|
1747
|
+
body: {
|
|
1748
|
+
storage: { value: body, representation }
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
async addLabel(pageId, labels) {
|
|
1754
|
+
const body = labels.map((name) => ({ prefix: "global", name }));
|
|
1755
|
+
const result = await this.http.confluence(`${API3}/content/${pageId}/label`, {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
body
|
|
1758
|
+
});
|
|
1759
|
+
return result.results;
|
|
1760
|
+
}
|
|
1761
|
+
async removeLabel(pageId, label) {
|
|
1762
|
+
return this.http.confluence(`${API3}/content/${pageId}/label/${label}`, { method: "DELETE" });
|
|
1763
|
+
}
|
|
1764
|
+
async listSpaces(opts = {}) {
|
|
1765
|
+
const initialStart = opts.start ?? 0;
|
|
1766
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1767
|
+
return this.http.confluence(`${API3}/space`, {
|
|
1768
|
+
params: {
|
|
1769
|
+
start: initialStart + start,
|
|
1770
|
+
limit: opts.limit ?? limit,
|
|
1771
|
+
...opts.type ? { type: opts.type } : {}
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
async getSpace(key) {
|
|
1777
|
+
return this.http.confluence(`${API3}/space/${key}`, {
|
|
1778
|
+
params: { expand: "description.plain" }
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
async listAttachments(pageId) {
|
|
1782
|
+
return this.http.confluencePaginate(async (start, limit) => {
|
|
1783
|
+
return this.http.confluence(`${API3}/content/${pageId}/child/attachment`, {
|
|
1784
|
+
params: { start, limit }
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1415
1790
|
// src/services/confluence/commands.ts
|
|
1791
|
+
function getClient3(program2) {
|
|
1792
|
+
const opts = program2.optsWithGlobals();
|
|
1793
|
+
const config = loadConfig({ configPath: opts.config });
|
|
1794
|
+
if (!config.confluence.baseUrl) throw new PncliError("Confluence not configured. Run: pncli config init");
|
|
1795
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
1796
|
+
return new ConfluenceClient(http);
|
|
1797
|
+
}
|
|
1416
1798
|
function registerConfluenceCommands(program2) {
|
|
1417
|
-
program2.command("confluence").description("Confluence operations")
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1799
|
+
const confluence = program2.command("confluence").description("Confluence operations");
|
|
1800
|
+
confluence.command("get-page").description("Get a Confluence page by ID").requiredOption("--id <page-id>", "Page ID").option("--expand <fields>", "Comma-separated fields to expand", "body.storage,version,space,ancestors").action(async (opts) => {
|
|
1801
|
+
const start = Date.now();
|
|
1802
|
+
try {
|
|
1803
|
+
const client = getClient3(program2);
|
|
1804
|
+
const data = await client.getPage(opts.id, opts.expand);
|
|
1805
|
+
success(data, "confluence", "get-page", start);
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
fail(err, "confluence", "get-page", start);
|
|
1808
|
+
}
|
|
1424
1809
|
});
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
"
|
|
1434
|
-
|
|
1435
|
-
);
|
|
1810
|
+
confluence.command("get-page-by-title").description("Find a Confluence page by space key and title").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").action(async (opts) => {
|
|
1811
|
+
const start = Date.now();
|
|
1812
|
+
try {
|
|
1813
|
+
const client = getClient3(program2);
|
|
1814
|
+
const data = await client.getPageByTitle(opts.space, opts.title);
|
|
1815
|
+
if (!data) throw new PncliError(`Page not found: "${opts.title}" in space ${opts.space}`, 1);
|
|
1816
|
+
success(data, "confluence", "get-page-by-title", start);
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
fail(err, "confluence", "get-page-by-title", start);
|
|
1819
|
+
}
|
|
1436
1820
|
});
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
"
|
|
1446
|
-
|
|
1447
|
-
|
|
1821
|
+
confluence.command("list-pages").description("List pages in a Confluence space").requiredOption("--space <key>", "Space key").option("--limit <n>", "Max results per page (default: all)").option("--start <n>", "Offset for first result").action(async (opts) => {
|
|
1822
|
+
const start = Date.now();
|
|
1823
|
+
try {
|
|
1824
|
+
const client = getClient3(program2);
|
|
1825
|
+
const data = await client.listPages(opts.space, {
|
|
1826
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : void 0,
|
|
1827
|
+
start: opts.start ? parseInt(opts.start, 10) : void 0
|
|
1828
|
+
});
|
|
1829
|
+
success(data, "confluence", "list-pages", start);
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
fail(err, "confluence", "list-pages", start);
|
|
1832
|
+
}
|
|
1448
1833
|
});
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
// src/services/config/commands.ts
|
|
1452
|
-
import { input, password, confirm } from "@inquirer/prompts";
|
|
1453
|
-
import fs2 from "fs";
|
|
1454
|
-
function registerConfigCommands(program2) {
|
|
1455
|
-
const config = program2.command("config").description("Manage pncli configuration");
|
|
1456
|
-
config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
|
|
1834
|
+
confluence.command("get-page-children").description("Get child pages of a Confluence page").requiredOption("--id <page-id>", "Parent page ID").action(async (opts) => {
|
|
1457
1835
|
const start = Date.now();
|
|
1458
1836
|
try {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
await initGlobalConfig(start);
|
|
1463
|
-
}
|
|
1837
|
+
const client = getClient3(program2);
|
|
1838
|
+
const data = await client.getPageChildren(opts.id);
|
|
1839
|
+
success(data, "confluence", "get-page-children", start);
|
|
1464
1840
|
} catch (err) {
|
|
1465
|
-
|
|
1466
|
-
process.stderr.write("\nSetup cancelled.\n");
|
|
1467
|
-
process.exit(1);
|
|
1468
|
-
}
|
|
1469
|
-
fail(err, "config", "init", start);
|
|
1841
|
+
fail(err, "confluence", "get-page-children", start);
|
|
1470
1842
|
}
|
|
1471
1843
|
});
|
|
1472
|
-
|
|
1844
|
+
confluence.command("get-labels").description("Get labels on a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
|
|
1473
1845
|
const start = Date.now();
|
|
1474
1846
|
try {
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1847
|
+
const client = getClient3(program2);
|
|
1848
|
+
const data = await client.getLabels(opts.id);
|
|
1849
|
+
success(data, "confluence", "get-labels", start);
|
|
1477
1850
|
} catch (err) {
|
|
1478
|
-
fail(err, "
|
|
1851
|
+
fail(err, "confluence", "get-labels", start);
|
|
1479
1852
|
}
|
|
1480
1853
|
});
|
|
1481
|
-
|
|
1854
|
+
confluence.command("search").description("Search Confluence with CQL").requiredOption("--cql <query>", 'CQL query string (e.g. "space=PROJ AND type=page")').option("--limit <n>", "Maximum number of results", "25").option("--start <n>", "Offset for first result", "0").option("--expand <fields>", "Comma-separated fields to expand").action(async (opts) => {
|
|
1482
1855
|
const start = Date.now();
|
|
1483
1856
|
try {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1857
|
+
const client = getClient3(program2);
|
|
1858
|
+
const data = await client.search(opts.cql, {
|
|
1859
|
+
limit: parseInt(opts.limit, 10),
|
|
1860
|
+
start: parseInt(opts.start, 10),
|
|
1861
|
+
expand: opts.expand
|
|
1862
|
+
});
|
|
1863
|
+
success(data, "confluence", "search", start);
|
|
1487
1864
|
} catch (err) {
|
|
1488
|
-
fail(err, "
|
|
1865
|
+
fail(err, "confluence", "search", start);
|
|
1489
1866
|
}
|
|
1490
1867
|
});
|
|
1491
|
-
|
|
1868
|
+
confluence.command("create-page").description("Create a new Confluence page").requiredOption("--space <key>", "Space key").requiredOption("--title <title>", "Page title").requiredOption("--body <html>", "Page body (storage format HTML)").option("--parent-id <id>", "Parent page ID (to nest under a page)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
|
|
1492
1869
|
const start = Date.now();
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1870
|
+
try {
|
|
1871
|
+
const client = getClient3(program2);
|
|
1872
|
+
const data = await client.createPage({
|
|
1873
|
+
spaceKey: opts.space,
|
|
1874
|
+
title: opts.title,
|
|
1875
|
+
body: opts.body,
|
|
1876
|
+
parentId: opts.parentId,
|
|
1877
|
+
representation: opts.representation
|
|
1878
|
+
});
|
|
1879
|
+
success(data, "confluence", "create-page", start);
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
fail(err, "confluence", "create-page", start);
|
|
1882
|
+
}
|
|
1499
1883
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1884
|
+
confluence.command("update-page").description("Update a Confluence page (fetches current version automatically)").requiredOption("--id <page-id>", "Page ID").option("--title <title>", "New page title").option("--body <html>", "New page body (storage format HTML)").option("--status <status>", "Page status: current (default) or draft", "current").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
|
|
1885
|
+
const start = Date.now();
|
|
1886
|
+
try {
|
|
1887
|
+
const client = getClient3(program2);
|
|
1888
|
+
const current = await client.getPage(opts.id, "version");
|
|
1889
|
+
const nextVersion = current.version.number + 1;
|
|
1890
|
+
const data = await client.updatePage(opts.id, {
|
|
1891
|
+
version: nextVersion,
|
|
1892
|
+
title: opts.title ?? current.title,
|
|
1893
|
+
body: opts.body,
|
|
1894
|
+
status: opts.status,
|
|
1895
|
+
representation: opts.representation
|
|
1896
|
+
});
|
|
1897
|
+
success(data, "confluence", "update-page", start);
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
fail(err, "confluence", "update-page", start);
|
|
1900
|
+
}
|
|
1507
1901
|
});
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1902
|
+
confluence.command("delete-page").description("Delete a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
|
|
1903
|
+
const start = Date.now();
|
|
1904
|
+
try {
|
|
1905
|
+
const client = getClient3(program2);
|
|
1906
|
+
await client.deletePage(opts.id);
|
|
1907
|
+
success({ deleted: opts.id }, "confluence", "delete-page", start);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
fail(err, "confluence", "delete-page", start);
|
|
1910
|
+
}
|
|
1511
1911
|
});
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1912
|
+
confluence.command("list-comments").description("List comments on a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
|
|
1913
|
+
const start = Date.now();
|
|
1914
|
+
try {
|
|
1915
|
+
const client = getClient3(program2);
|
|
1916
|
+
const data = await client.listComments(opts.id);
|
|
1917
|
+
success(data, "confluence", "list-comments", start);
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
fail(err, "confluence", "list-comments", start);
|
|
1920
|
+
}
|
|
1516
1921
|
});
|
|
1517
|
-
|
|
1518
|
-
|
|
1922
|
+
confluence.command("add-comment").description("Add a comment to a Confluence page").requiredOption("--id <page-id>", "Page ID").requiredOption("--body <text>", "Comment body (storage format HTML)").option("--representation <format>", "Body format: storage (default) or wiki", "storage").action(async (opts) => {
|
|
1923
|
+
const start = Date.now();
|
|
1924
|
+
try {
|
|
1925
|
+
const client = getClient3(program2);
|
|
1926
|
+
const data = await client.addComment(opts.id, opts.body, opts.representation);
|
|
1927
|
+
success(data, "confluence", "add-comment", start);
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
fail(err, "confluence", "add-comment", start);
|
|
1930
|
+
}
|
|
1519
1931
|
});
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1932
|
+
confluence.command("add-label").description("Add labels to a Confluence page").requiredOption("--id <page-id>", "Page ID").requiredOption("--labels <names>", "Comma-separated label names").action(async (opts) => {
|
|
1933
|
+
const start = Date.now();
|
|
1934
|
+
try {
|
|
1935
|
+
const client = getClient3(program2);
|
|
1936
|
+
const labels = opts.labels.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1937
|
+
const data = await client.addLabel(opts.id, labels);
|
|
1938
|
+
success(data, "confluence", "add-label", start);
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
fail(err, "confluence", "add-label", start);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
confluence.command("remove-label").description("Remove a label from a Confluence page").requiredOption("--id <page-id>", "Page ID").requiredOption("--label <name>", "Label name to remove").action(async (opts) => {
|
|
1944
|
+
const start = Date.now();
|
|
1945
|
+
try {
|
|
1946
|
+
const client = getClient3(program2);
|
|
1947
|
+
await client.removeLabel(opts.id, opts.label);
|
|
1948
|
+
success({ removed: opts.label, from: opts.id }, "confluence", "remove-label", start);
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
fail(err, "confluence", "remove-label", start);
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
confluence.command("list-spaces").description("List Confluence spaces").option("--type <type>", "Space type: global or personal").option("--limit <n>", "Max results (default: all)").action(async (opts) => {
|
|
1954
|
+
const start = Date.now();
|
|
1955
|
+
try {
|
|
1956
|
+
const client = getClient3(program2);
|
|
1957
|
+
const data = await client.listSpaces({
|
|
1958
|
+
type: opts.type,
|
|
1959
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : void 0
|
|
1960
|
+
});
|
|
1961
|
+
success(data, "confluence", "list-spaces", start);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
fail(err, "confluence", "list-spaces", start);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
confluence.command("get-space").description("Get a Confluence space by key").requiredOption("--key <space-key>", "Space key").action(async (opts) => {
|
|
1967
|
+
const start = Date.now();
|
|
1968
|
+
try {
|
|
1969
|
+
const client = getClient3(program2);
|
|
1970
|
+
const data = await client.getSpace(opts.key);
|
|
1971
|
+
success(data, "confluence", "get-space", start);
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
fail(err, "confluence", "get-space", start);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
confluence.command("list-attachments").description("List attachments on a Confluence page").requiredOption("--id <page-id>", "Page ID").action(async (opts) => {
|
|
1977
|
+
const start = Date.now();
|
|
1978
|
+
try {
|
|
1979
|
+
const client = getClient3(program2);
|
|
1980
|
+
const data = await client.listAttachments(opts.id);
|
|
1981
|
+
success(data, "confluence", "list-attachments", start);
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
fail(err, "confluence", "list-attachments", start);
|
|
1984
|
+
}
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// src/services/sonar/client.ts
|
|
1989
|
+
var API4 = "/api";
|
|
1990
|
+
var SonarClient = class {
|
|
1991
|
+
constructor(http) {
|
|
1992
|
+
this.http = http;
|
|
1993
|
+
}
|
|
1994
|
+
http;
|
|
1995
|
+
async getStatus() {
|
|
1996
|
+
return this.http.sonar(`${API4}/system/status`);
|
|
1997
|
+
}
|
|
1998
|
+
async getQualityGate(projectKey, branch) {
|
|
1999
|
+
return this.http.sonar(`${API4}/qualitygates/project_status`, {
|
|
2000
|
+
params: { projectKey, ...branch ? { branch } : {} }
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
async searchIssues(opts) {
|
|
2004
|
+
return this.http.sonar(`${API4}/issues/search`, {
|
|
2005
|
+
params: {
|
|
2006
|
+
componentKeys: opts.projectKey,
|
|
2007
|
+
severities: opts.severities,
|
|
2008
|
+
types: opts.types,
|
|
2009
|
+
statuses: opts.statuses,
|
|
2010
|
+
branch: opts.branch,
|
|
2011
|
+
resolved: opts.resolved !== void 0 ? String(opts.resolved) : void 0,
|
|
2012
|
+
p: opts.page ?? 1,
|
|
2013
|
+
ps: opts.pageSize ?? 100
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
async searchAllIssues(opts) {
|
|
2018
|
+
return this.http.sonarPaginate(async (page, pageSize) => {
|
|
2019
|
+
const resp = await this.http.sonar(`${API4}/issues/search`, {
|
|
2020
|
+
params: {
|
|
2021
|
+
componentKeys: opts.projectKey,
|
|
2022
|
+
severities: opts.severities,
|
|
2023
|
+
types: opts.types,
|
|
2024
|
+
statuses: opts.statuses,
|
|
2025
|
+
branch: opts.branch,
|
|
2026
|
+
resolved: opts.resolved !== void 0 ? String(opts.resolved) : void 0,
|
|
2027
|
+
p: page,
|
|
2028
|
+
ps: pageSize
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
return { total: resp.total, p: resp.p, ps: resp.ps, items: resp.issues };
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
async getMeasures(opts) {
|
|
2035
|
+
return this.http.sonar(`${API4}/measures/component`, {
|
|
2036
|
+
params: {
|
|
2037
|
+
component: opts.projectKey,
|
|
2038
|
+
metricKeys: opts.metricKeys,
|
|
2039
|
+
...opts.branch ? { branch: opts.branch } : {}
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
async searchProjects(opts = {}) {
|
|
2044
|
+
return this.http.sonar(`${API4}/projects/search`, {
|
|
2045
|
+
params: {
|
|
2046
|
+
q: opts.query,
|
|
2047
|
+
p: opts.page ?? 1,
|
|
2048
|
+
ps: opts.pageSize ?? 100
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
async searchAllProjects(query) {
|
|
2053
|
+
return this.http.sonarPaginate(async (page, pageSize) => {
|
|
2054
|
+
const resp = await this.http.sonar(`${API4}/projects/search`, {
|
|
2055
|
+
params: { q: query, p: page, ps: pageSize }
|
|
2056
|
+
});
|
|
2057
|
+
return { total: resp.paging.total, p: resp.paging.pageIndex, ps: resp.paging.pageSize, items: resp.components };
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
async searchHotspots(opts) {
|
|
2061
|
+
return this.http.sonar(`${API4}/hotspots/search`, {
|
|
2062
|
+
params: {
|
|
2063
|
+
projectKey: opts.projectKey,
|
|
2064
|
+
status: opts.status,
|
|
2065
|
+
resolution: opts.resolution,
|
|
2066
|
+
branch: opts.branch,
|
|
2067
|
+
p: opts.page ?? 1,
|
|
2068
|
+
ps: opts.pageSize ?? 100
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
async searchAllHotspots(opts) {
|
|
2073
|
+
return this.http.sonarPaginate(async (page, pageSize) => {
|
|
2074
|
+
const resp = await this.http.sonar(`${API4}/hotspots/search`, {
|
|
2075
|
+
params: {
|
|
2076
|
+
projectKey: opts.projectKey,
|
|
2077
|
+
status: opts.status,
|
|
2078
|
+
resolution: opts.resolution,
|
|
2079
|
+
branch: opts.branch,
|
|
2080
|
+
p: page,
|
|
2081
|
+
ps: pageSize
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
return { total: resp.paging.total, p: resp.paging.pageIndex, ps: resp.paging.pageSize, items: resp.hotspots };
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
// src/services/sonar/commands.ts
|
|
2090
|
+
var DEFAULT_METRICS = "coverage,duplicated_lines_density,bugs,vulnerabilities,code_smells,sqale_rating,reliability_rating,security_rating,ncloc";
|
|
2091
|
+
function getClient4(program2) {
|
|
2092
|
+
const opts = program2.optsWithGlobals();
|
|
2093
|
+
const config = loadConfig({ configPath: opts.config });
|
|
2094
|
+
if (!config.sonar.baseUrl) throw new PncliError("SonarQube not configured. Run: pncli config init");
|
|
2095
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
2096
|
+
return { client: new SonarClient(http), config };
|
|
2097
|
+
}
|
|
2098
|
+
function resolveProject(config, cliProject) {
|
|
2099
|
+
const project = cliProject ?? config.defaults.sonar.project;
|
|
2100
|
+
if (!project) throw new PncliError("No project specified. Use --project or set defaults.sonar.project in config.");
|
|
2101
|
+
return project;
|
|
2102
|
+
}
|
|
2103
|
+
function registerSonarCommands(program2) {
|
|
2104
|
+
const sonar = program2.command("sonar").description("SonarQube Server operations");
|
|
2105
|
+
sonar.command("quality-gate").description("Get quality gate status for a project").option("--project <key>", "SonarQube project key (or set defaults.sonar.project in config)").option("--branch <name>", "Branch name").action(async (opts) => {
|
|
2106
|
+
const start = Date.now();
|
|
2107
|
+
try {
|
|
2108
|
+
const { client, config } = getClient4(program2);
|
|
2109
|
+
const projectKey = resolveProject(config, opts.project);
|
|
2110
|
+
const data = await client.getQualityGate(projectKey, opts.branch);
|
|
2111
|
+
success(data, "sonar", "quality-gate", start);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
fail(err, "sonar", "quality-gate", start);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
sonar.command("issues").description("Search/list issues for a project").option("--project <key>", "SonarQube project key (or set defaults.sonar.project in config)").option("--severities <list>", "Filter by severity: BLOCKER,CRITICAL,MAJOR,MINOR,INFO (comma-separated)").option("--types <list>", "Filter by type: BUG,VULNERABILITY,CODE_SMELL (comma-separated)").option("--statuses <list>", "Filter by status: OPEN,CONFIRMED,REOPENED,RESOLVED,CLOSED (comma-separated)").option("--branch <name>", "Branch name").option("--resolved <bool>", "Filter resolved issues: true or false").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page (max 500)", "100").option("--all", "Fetch all pages (ignores --page/--page-size)").action(async (opts) => {
|
|
2117
|
+
const start = Date.now();
|
|
2118
|
+
try {
|
|
2119
|
+
const { client, config } = getClient4(program2);
|
|
2120
|
+
const projectKey = resolveProject(config, opts.project);
|
|
2121
|
+
const baseOpts = {
|
|
2122
|
+
projectKey,
|
|
2123
|
+
severities: opts.severities,
|
|
2124
|
+
types: opts.types,
|
|
2125
|
+
statuses: opts.statuses,
|
|
2126
|
+
branch: opts.branch,
|
|
2127
|
+
resolved: opts.resolved !== void 0 ? opts.resolved === "true" : void 0
|
|
2128
|
+
};
|
|
2129
|
+
if (opts.all) {
|
|
2130
|
+
const data = await client.searchAllIssues(baseOpts);
|
|
2131
|
+
success(data, "sonar", "issues", start);
|
|
2132
|
+
} else {
|
|
2133
|
+
const data = await client.searchIssues({
|
|
2134
|
+
...baseOpts,
|
|
2135
|
+
page: parseInt(opts.page, 10),
|
|
2136
|
+
pageSize: parseInt(opts.pageSize, 10)
|
|
2137
|
+
});
|
|
2138
|
+
success(data, "sonar", "issues", start);
|
|
2139
|
+
}
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
fail(err, "sonar", "issues", start);
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
sonar.command("measures").description("Get key metrics for a project").option("--project <key>", "SonarQube project key (or set defaults.sonar.project in config)").option("--metrics <list>", "Comma-separated metric keys", DEFAULT_METRICS).option("--branch <name>", "Branch name").action(async (opts) => {
|
|
2145
|
+
const start = Date.now();
|
|
2146
|
+
try {
|
|
2147
|
+
const { client, config } = getClient4(program2);
|
|
2148
|
+
const projectKey = resolveProject(config, opts.project);
|
|
2149
|
+
const data = await client.getMeasures({
|
|
2150
|
+
projectKey,
|
|
2151
|
+
metricKeys: opts.metrics,
|
|
2152
|
+
branch: opts.branch
|
|
2153
|
+
});
|
|
2154
|
+
success(data, "sonar", "measures", start);
|
|
2155
|
+
} catch (err) {
|
|
2156
|
+
fail(err, "sonar", "measures", start);
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
sonar.command("projects").description("Search/list projects").option("--query <text>", "Search query").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2160
|
+
const start = Date.now();
|
|
2161
|
+
try {
|
|
2162
|
+
const { client } = getClient4(program2);
|
|
2163
|
+
if (opts.all) {
|
|
2164
|
+
const data = await client.searchAllProjects(opts.query);
|
|
2165
|
+
success(data, "sonar", "projects", start);
|
|
2166
|
+
} else {
|
|
2167
|
+
const data = await client.searchProjects({
|
|
2168
|
+
query: opts.query,
|
|
2169
|
+
page: parseInt(opts.page, 10),
|
|
2170
|
+
pageSize: parseInt(opts.pageSize, 10)
|
|
2171
|
+
});
|
|
2172
|
+
success(data, "sonar", "projects", start);
|
|
2173
|
+
}
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
fail(err, "sonar", "projects", start);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
sonar.command("hotspots").description("List security hotspots for a project").option("--project <key>", "SonarQube project key (or set defaults.sonar.project in config)").option("--status <status>", "Filter: TO_REVIEW or REVIEWED").option("--resolution <list>", "Filter: FIXED,SAFE,ACKNOWLEDGED (comma-separated)").option("--branch <name>", "Branch name").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2179
|
+
const start = Date.now();
|
|
2180
|
+
try {
|
|
2181
|
+
const { client, config } = getClient4(program2);
|
|
2182
|
+
const projectKey = resolveProject(config, opts.project);
|
|
2183
|
+
const baseOpts = {
|
|
2184
|
+
projectKey,
|
|
2185
|
+
status: opts.status,
|
|
2186
|
+
resolution: opts.resolution,
|
|
2187
|
+
branch: opts.branch
|
|
2188
|
+
};
|
|
2189
|
+
if (opts.all) {
|
|
2190
|
+
const data = await client.searchAllHotspots(baseOpts);
|
|
2191
|
+
success(data, "sonar", "hotspots", start);
|
|
2192
|
+
} else {
|
|
2193
|
+
const data = await client.searchHotspots({
|
|
2194
|
+
...baseOpts,
|
|
2195
|
+
page: parseInt(opts.page, 10),
|
|
2196
|
+
pageSize: parseInt(opts.pageSize, 10)
|
|
2197
|
+
});
|
|
2198
|
+
success(data, "sonar", "hotspots", start);
|
|
2199
|
+
}
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
fail(err, "sonar", "hotspots", start);
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/services/sde/client.ts
|
|
2207
|
+
var API5 = "/api/v2";
|
|
2208
|
+
function projectParams(opts, page, pageSize) {
|
|
2209
|
+
return {
|
|
2210
|
+
name: opts.name,
|
|
2211
|
+
slug: opts.slug,
|
|
2212
|
+
search: opts.search,
|
|
2213
|
+
active: opts.active,
|
|
2214
|
+
ordering: opts.ordering,
|
|
2215
|
+
expand: opts.expand,
|
|
2216
|
+
include: opts.include,
|
|
2217
|
+
page,
|
|
2218
|
+
page_size: pageSize
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
function taskParams(opts, page, pageSize) {
|
|
2222
|
+
return {
|
|
2223
|
+
phase: opts.phase,
|
|
2224
|
+
priority: opts.priority,
|
|
2225
|
+
status: opts.status,
|
|
2226
|
+
assigned_to: opts.assignedTo,
|
|
2227
|
+
source: opts.source,
|
|
2228
|
+
verification: opts.verification,
|
|
2229
|
+
tag: opts.tag,
|
|
2230
|
+
accepted: opts.accepted,
|
|
2231
|
+
relevant: opts.relevant,
|
|
2232
|
+
expand: opts.expand,
|
|
2233
|
+
include: opts.include,
|
|
2234
|
+
page,
|
|
2235
|
+
page_size: pageSize
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
function threatParams(opts, page, pageSize) {
|
|
2239
|
+
return {
|
|
2240
|
+
severity: opts.severity,
|
|
2241
|
+
title: opts.title,
|
|
2242
|
+
threat_id: opts.threatId,
|
|
2243
|
+
capec_id: opts.capecId,
|
|
2244
|
+
component_id: opts.componentId,
|
|
2245
|
+
search: opts.search,
|
|
2246
|
+
ordering: opts.ordering,
|
|
2247
|
+
page,
|
|
2248
|
+
page_size: pageSize
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function userParams(opts, page, pageSize) {
|
|
2252
|
+
return {
|
|
2253
|
+
email: opts.email,
|
|
2254
|
+
first_name: opts.firstName,
|
|
2255
|
+
last_name: opts.lastName,
|
|
2256
|
+
is_active: opts.isActive,
|
|
2257
|
+
page,
|
|
2258
|
+
page_size: pageSize
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
var SdeClient = class {
|
|
2262
|
+
constructor(http) {
|
|
2263
|
+
this.http = http;
|
|
2264
|
+
}
|
|
2265
|
+
http;
|
|
2266
|
+
async getServerInfo() {
|
|
2267
|
+
return this.http.sde(`${API5}/server-info/`);
|
|
2268
|
+
}
|
|
2269
|
+
async getMe() {
|
|
2270
|
+
return this.http.sde(`${API5}/users/me/`);
|
|
2271
|
+
}
|
|
2272
|
+
async getProject(projectId, expand, include) {
|
|
2273
|
+
return this.http.sde(`${API5}/projects/${projectId}/`, {
|
|
2274
|
+
params: { expand, include }
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
async getTask(projectId, taskId, expand, include) {
|
|
2278
|
+
return this.http.sde(`${API5}/projects/${projectId}/tasks/${taskId}/`, {
|
|
2279
|
+
params: { expand, include }
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
async listProjects(opts = {}) {
|
|
2283
|
+
return this.http.sde(`${API5}/projects/`, {
|
|
2284
|
+
params: projectParams(opts, opts.page ?? 1, opts.pageSize ?? 100)
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async listAllProjects(opts = {}) {
|
|
2288
|
+
return this.http.sdePaginate(async (page, pageSize) => {
|
|
2289
|
+
const resp = await this.http.sde(`${API5}/projects/`, {
|
|
2290
|
+
params: projectParams(opts, page, pageSize)
|
|
2291
|
+
});
|
|
2292
|
+
return { count: resp.count, results: resp.results };
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
async listTasks(opts) {
|
|
2296
|
+
return this.http.sde(`${API5}/projects/${opts.projectId}/tasks/`, {
|
|
2297
|
+
params: taskParams(opts, opts.page ?? 1, opts.pageSize ?? 100)
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
async listAllTasks(opts) {
|
|
2301
|
+
return this.http.sdePaginate(async (page, pageSize) => {
|
|
2302
|
+
const resp = await this.http.sde(`${API5}/projects/${opts.projectId}/tasks/`, {
|
|
2303
|
+
params: taskParams(opts, page, pageSize)
|
|
2304
|
+
});
|
|
2305
|
+
return { count: resp.count, results: resp.results };
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
async listThreats(opts) {
|
|
2309
|
+
return this.http.sde(`${API5}/projects/${opts.projectId}/threats/`, {
|
|
2310
|
+
params: threatParams(opts, opts.page ?? 1, opts.pageSize ?? 100)
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
async listAllThreats(opts) {
|
|
2314
|
+
return this.http.sdePaginate(async (page, pageSize) => {
|
|
2315
|
+
const resp = await this.http.sde(`${API5}/projects/${opts.projectId}/threats/`, {
|
|
2316
|
+
params: threatParams(opts, page, pageSize)
|
|
2317
|
+
});
|
|
2318
|
+
return { count: resp.count, results: resp.results };
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
async listUsers(opts = {}) {
|
|
2322
|
+
return this.http.sde(`${API5}/users/`, {
|
|
2323
|
+
params: userParams(opts, opts.page ?? 1, opts.pageSize ?? 100)
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
async listAllUsers(opts = {}) {
|
|
2327
|
+
return this.http.sdePaginate(async (page, pageSize) => {
|
|
2328
|
+
const resp = await this.http.sde(`${API5}/users/`, {
|
|
2329
|
+
params: userParams(opts, page, pageSize)
|
|
2330
|
+
});
|
|
2331
|
+
return { count: resp.count, results: resp.results };
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// src/services/sde/commands.ts
|
|
2337
|
+
function getClient5(program2) {
|
|
2338
|
+
const opts = program2.optsWithGlobals();
|
|
2339
|
+
const config = loadConfig({ configPath: opts.config });
|
|
2340
|
+
if (!config.sde.baseUrl) throw new PncliError("SDElements not configured. Run: pncli config init");
|
|
2341
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
2342
|
+
return { client: new SdeClient(http), config };
|
|
2343
|
+
}
|
|
2344
|
+
function resolveProject2(config, cliProject) {
|
|
2345
|
+
const project = cliProject ?? config.defaults.sde.project;
|
|
2346
|
+
if (!project) throw new PncliError("No project specified. Use --project or set defaults.sde.project in config.");
|
|
2347
|
+
if (!/^\d+$/.test(project)) throw new PncliError(`Invalid project ID: "${project}". SDElements project IDs must be positive integers.`);
|
|
2348
|
+
const id = parseInt(project, 10);
|
|
2349
|
+
if (id <= 0) throw new PncliError(`Invalid project ID: "${project}". SDElements project IDs must be positive integers.`);
|
|
2350
|
+
return id;
|
|
2351
|
+
}
|
|
2352
|
+
function parsePage(val, flag) {
|
|
2353
|
+
const n = parseInt(val, 10);
|
|
2354
|
+
if (isNaN(n) || n < 1) throw new PncliError(`Invalid ${flag}: "${val}". Must be a positive integer.`);
|
|
2355
|
+
return n;
|
|
2356
|
+
}
|
|
2357
|
+
function validateActive(val, allowed) {
|
|
2358
|
+
if (val === void 0) return void 0;
|
|
2359
|
+
if (!allowed.includes(val)) {
|
|
2360
|
+
throw new PncliError(`Invalid --active value: "${val}". Allowed: ${allowed.join(", ")}.`);
|
|
2361
|
+
}
|
|
2362
|
+
return val;
|
|
2363
|
+
}
|
|
2364
|
+
function registerSdeCommands(program2) {
|
|
2365
|
+
const sde = program2.command("sde").description("SDElements operations (threat modeling, countermeasures, compliance)");
|
|
2366
|
+
sde.command("server-info").description("Get server version and platform info (requires super-user)").action(async () => {
|
|
2367
|
+
const start = Date.now();
|
|
2368
|
+
try {
|
|
2369
|
+
const { client } = getClient5(program2);
|
|
2370
|
+
const data = await client.getServerInfo();
|
|
2371
|
+
success(data, "sde", "server-info", start);
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
fail(err, "sde", "server-info", start);
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
sde.command("whoami").description("Get current authenticated user").action(async () => {
|
|
2377
|
+
const start = Date.now();
|
|
2378
|
+
try {
|
|
2379
|
+
const { client } = getClient5(program2);
|
|
2380
|
+
const data = await client.getMe();
|
|
2381
|
+
success(data, "sde", "whoami", start);
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
fail(err, "sde", "whoami", start);
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
sde.command("users").description("List users").option("--email <email>", "Filter by email address").option("--first-name <name>", "Filter by first name").option("--last-name <name>", "Filter by last name").option("--active <bool>", "Filter by active status: true or false").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2387
|
+
const start = Date.now();
|
|
2388
|
+
try {
|
|
2389
|
+
const { client } = getClient5(program2);
|
|
2390
|
+
const baseOpts = {
|
|
2391
|
+
email: opts.email,
|
|
2392
|
+
firstName: opts.firstName,
|
|
2393
|
+
lastName: opts.lastName,
|
|
2394
|
+
isActive: validateActive(opts.active, ["true", "false"])
|
|
2395
|
+
};
|
|
2396
|
+
if (opts.all) {
|
|
2397
|
+
const data = await client.listAllUsers(baseOpts);
|
|
2398
|
+
success(data, "sde", "users", start);
|
|
2399
|
+
} else {
|
|
2400
|
+
const data = await client.listUsers({
|
|
2401
|
+
...baseOpts,
|
|
2402
|
+
page: parsePage(opts.page, "--page"),
|
|
2403
|
+
pageSize: parsePage(opts.pageSize, "--page-size")
|
|
2404
|
+
});
|
|
2405
|
+
success(data, "sde", "users", start);
|
|
2406
|
+
}
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
fail(err, "sde", "users", start);
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
sde.command("projects").description("List projects").option("--name <name>", "Filter by project name").option("--search <text>", "Text search on name and profile").option("--active <val>", "Filter by active status: true, false, or all").option("--ordering <field>", "Sort by: name, created, updated (prefix with - for descending)").option("--expand <fields>", "Expand nested fields (comma-separated): application,business_unit,creator").option("--include <fields>", "Include extra fields (comma-separated): task_counts,permissions").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2412
|
+
const start = Date.now();
|
|
2413
|
+
try {
|
|
2414
|
+
const { client } = getClient5(program2);
|
|
2415
|
+
const baseOpts = {
|
|
2416
|
+
name: opts.name,
|
|
2417
|
+
search: opts.search,
|
|
2418
|
+
active: validateActive(opts.active, ["true", "false", "all"]),
|
|
2419
|
+
ordering: opts.ordering,
|
|
2420
|
+
expand: opts.expand,
|
|
2421
|
+
include: opts.include
|
|
2422
|
+
};
|
|
2423
|
+
if (opts.all) {
|
|
2424
|
+
const data = await client.listAllProjects(baseOpts);
|
|
2425
|
+
success(data, "sde", "projects", start);
|
|
2426
|
+
} else {
|
|
2427
|
+
const data = await client.listProjects({
|
|
2428
|
+
...baseOpts,
|
|
2429
|
+
page: parsePage(opts.page, "--page"),
|
|
2430
|
+
pageSize: parsePage(opts.pageSize, "--page-size")
|
|
2431
|
+
});
|
|
2432
|
+
success(data, "sde", "projects", start);
|
|
2433
|
+
}
|
|
2434
|
+
} catch (err) {
|
|
2435
|
+
fail(err, "sde", "projects", start);
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
sde.command("project").description("Get a single project by ID").option("--id <id>", "Project ID (or set defaults.sde.project in config)").option("--expand <fields>", "Expand nested fields (comma-separated): application,business_unit,creator").option("--include <fields>", "Include extra fields (comma-separated): task_counts,permissions").action(async (opts) => {
|
|
2439
|
+
const start = Date.now();
|
|
2440
|
+
try {
|
|
2441
|
+
const { client, config } = getClient5(program2);
|
|
2442
|
+
const projectId = resolveProject2(config, opts.id);
|
|
2443
|
+
const data = await client.getProject(projectId, opts.expand, opts.include);
|
|
2444
|
+
success(data, "sde", "project", start);
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
fail(err, "sde", "project", start);
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
sde.command("tasks").description("List countermeasures for a project").option("--project <id>", "Project ID (or set defaults.sde.project in config)").option("--phase <slug>", "Filter by phase slug (e.g. development, architecture-design)").option("--priority <n>", "Filter by priority (1-10)").option("--status <id>", "Filter by status ID (e.g. TS1, TS2)").option("--assigned-to <email>", "Filter by assignee email").option("--source <val>", "Filter by source: default, custom, manual, project").option("--verification <val>", "Filter by verification: pass, fail, partial, none").option("--tag <name>", "Filter by tag name").option("--accepted <bool>", "Filter by accepted status: true or false").option("--relevant <bool>", "Filter by relevant status: true or false").option("--expand <fields>", "Expand nested fields (comma-separated): status,phase,problem,text").option("--include <fields>", "Include extra fields (comma-separated): how_tos,last_note,references,regulation_sections").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2450
|
+
const start = Date.now();
|
|
2451
|
+
try {
|
|
2452
|
+
const { client, config } = getClient5(program2);
|
|
2453
|
+
const projectId = resolveProject2(config, opts.project);
|
|
2454
|
+
const baseOpts = {
|
|
2455
|
+
projectId,
|
|
2456
|
+
phase: opts.phase,
|
|
2457
|
+
priority: opts.priority,
|
|
2458
|
+
status: opts.status,
|
|
2459
|
+
assignedTo: opts.assignedTo,
|
|
2460
|
+
source: opts.source,
|
|
2461
|
+
verification: opts.verification,
|
|
2462
|
+
tag: opts.tag,
|
|
2463
|
+
accepted: opts.accepted,
|
|
2464
|
+
relevant: opts.relevant,
|
|
2465
|
+
expand: opts.expand,
|
|
2466
|
+
include: opts.include
|
|
2467
|
+
};
|
|
2468
|
+
if (opts.all) {
|
|
2469
|
+
const data = await client.listAllTasks(baseOpts);
|
|
2470
|
+
success(data, "sde", "tasks", start);
|
|
2471
|
+
} else {
|
|
2472
|
+
const data = await client.listTasks({
|
|
2473
|
+
...baseOpts,
|
|
2474
|
+
page: parsePage(opts.page, "--page"),
|
|
2475
|
+
pageSize: parsePage(opts.pageSize, "--page-size")
|
|
2476
|
+
});
|
|
2477
|
+
success(data, "sde", "tasks", start);
|
|
2478
|
+
}
|
|
2479
|
+
} catch (err) {
|
|
2480
|
+
fail(err, "sde", "tasks", start);
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
sde.command("task").description("Get a single countermeasure").option("--project <id>", "Project ID (or set defaults.sde.project in config)").requiredOption("--task <id>", "Task ID (e.g. T21)").option("--expand <fields>", "Expand nested fields (comma-separated): status,phase,problem,text").option("--include <fields>", "Include extra fields (comma-separated): how_tos,last_note,references").action(async (opts) => {
|
|
2484
|
+
const start = Date.now();
|
|
2485
|
+
try {
|
|
2486
|
+
const { client, config } = getClient5(program2);
|
|
2487
|
+
const projectId = resolveProject2(config, opts.project);
|
|
2488
|
+
const data = await client.getTask(projectId, opts.task, opts.expand, opts.include);
|
|
2489
|
+
success(data, "sde", "task", start);
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
fail(err, "sde", "task", start);
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
sde.command("threats").description("List threats for a project").option("--project <id>", "Project ID (or set defaults.sde.project in config)").option("--severity <n>", "Filter by severity (1-10)").option("--search <text>", "Full-text search on title and threat ID").option("--ordering <field>", "Sort by: threat__severity, threat_id, status (prefix - for descending)").option("--capec-id <id>", "Filter by CAPEC attack pattern ID").option("--component-id <id>", "Filter by component ID").option("--page <n>", "Page number (1-based)", "1").option("--page-size <n>", "Results per page", "100").option("--all", "Fetch all pages").action(async (opts) => {
|
|
2495
|
+
const start = Date.now();
|
|
2496
|
+
try {
|
|
2497
|
+
const { client, config } = getClient5(program2);
|
|
2498
|
+
const projectId = resolveProject2(config, opts.project);
|
|
2499
|
+
const baseOpts = {
|
|
2500
|
+
projectId,
|
|
2501
|
+
severity: opts.severity,
|
|
2502
|
+
search: opts.search,
|
|
2503
|
+
ordering: opts.ordering,
|
|
2504
|
+
capecId: opts.capecId,
|
|
2505
|
+
componentId: opts.componentId
|
|
2506
|
+
};
|
|
2507
|
+
if (opts.all) {
|
|
2508
|
+
const data = await client.listAllThreats(baseOpts);
|
|
2509
|
+
success(data, "sde", "threats", start);
|
|
2510
|
+
} else {
|
|
2511
|
+
const data = await client.listThreats({
|
|
2512
|
+
...baseOpts,
|
|
2513
|
+
page: parsePage(opts.page, "--page"),
|
|
2514
|
+
pageSize: parsePage(opts.pageSize, "--page-size")
|
|
2515
|
+
});
|
|
2516
|
+
success(data, "sde", "threats", start);
|
|
2517
|
+
}
|
|
2518
|
+
} catch (err) {
|
|
2519
|
+
fail(err, "sde", "threats", start);
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// src/services/deps/parsers/index.ts
|
|
2525
|
+
import fs4 from "fs";
|
|
2526
|
+
import path5 from "path";
|
|
2527
|
+
import { execSync as execSync4, execFileSync as execFileSync2 } from "child_process";
|
|
2528
|
+
|
|
2529
|
+
// src/services/deps/parsers/npm.ts
|
|
2530
|
+
import path2 from "path";
|
|
2531
|
+
function findNpmManifests(files) {
|
|
2532
|
+
const lockFiles = new Set(
|
|
2533
|
+
files.filter((f) => {
|
|
2534
|
+
const base = path2.basename(f);
|
|
2535
|
+
return base === "package-lock.json" || base === "yarn.lock" || base === "pnpm-lock.yaml";
|
|
2536
|
+
})
|
|
2537
|
+
);
|
|
2538
|
+
const manifests = [];
|
|
2539
|
+
for (const file of files) {
|
|
2540
|
+
if (path2.basename(file) !== "package.json") continue;
|
|
2541
|
+
if (file.split("/").includes("node_modules")) continue;
|
|
2542
|
+
const dir = path2.dirname(file);
|
|
2543
|
+
let lockFile;
|
|
2544
|
+
for (const lf of ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]) {
|
|
2545
|
+
const candidate = dir === "." ? lf : `${dir}/${lf}`;
|
|
2546
|
+
if (lockFiles.has(candidate)) {
|
|
2547
|
+
lockFile = candidate;
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
manifests.push({ file, ecosystem: "npm", lockFile });
|
|
2552
|
+
}
|
|
2553
|
+
return manifests;
|
|
2554
|
+
}
|
|
2555
|
+
function parseNpmPackages(content, manifest, opts, lockContent) {
|
|
2556
|
+
if (lockContent && manifest.lockFile) {
|
|
2557
|
+
const base = path2.basename(manifest.lockFile);
|
|
2558
|
+
if (base === "package-lock.json") {
|
|
2559
|
+
const pkgs = parsePackageLock(lockContent, manifest.lockFile, opts);
|
|
2560
|
+
if (pkgs.length > 0) return pkgs;
|
|
2561
|
+
} else if (base === "yarn.lock") {
|
|
2562
|
+
return parseYarnLock(lockContent, manifest.lockFile, opts, content);
|
|
2563
|
+
} else if (base === "pnpm-lock.yaml") {
|
|
2564
|
+
return parsePnpmLock(lockContent, manifest.lockFile, opts, content);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return parsePackageJson(content, manifest.file, opts);
|
|
2568
|
+
}
|
|
2569
|
+
function stripVersionPrefix(v) {
|
|
2570
|
+
return v.replace(/^[\^~>=<* ]+/, "").split(/\s/)[0] ?? v;
|
|
2571
|
+
}
|
|
2572
|
+
function parsePackageJson(content, filePath, opts) {
|
|
2573
|
+
let pkg2;
|
|
2574
|
+
try {
|
|
2575
|
+
pkg2 = JSON.parse(content);
|
|
2576
|
+
} catch {
|
|
2577
|
+
return [];
|
|
2578
|
+
}
|
|
2579
|
+
const packages = [];
|
|
2580
|
+
for (const [name, version] of Object.entries(pkg2.dependencies ?? {})) {
|
|
2581
|
+
packages.push({
|
|
2582
|
+
name,
|
|
2583
|
+
version: stripVersionPrefix(version),
|
|
2584
|
+
ecosystem: "npm",
|
|
2585
|
+
source: filePath,
|
|
2586
|
+
type: "direct",
|
|
2587
|
+
scope: "production"
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
if (opts.includeDev) {
|
|
2591
|
+
for (const [name, version] of Object.entries(pkg2.devDependencies ?? {})) {
|
|
2592
|
+
packages.push({
|
|
2593
|
+
name,
|
|
2594
|
+
version: stripVersionPrefix(version),
|
|
2595
|
+
ecosystem: "npm",
|
|
2596
|
+
source: filePath,
|
|
2597
|
+
type: "direct",
|
|
2598
|
+
scope: "dev"
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return packages;
|
|
2603
|
+
}
|
|
2604
|
+
function parsePackageLock(content, lockFilePath, opts) {
|
|
2605
|
+
let lock;
|
|
2606
|
+
try {
|
|
2607
|
+
lock = JSON.parse(content);
|
|
2608
|
+
} catch {
|
|
2609
|
+
return [];
|
|
2610
|
+
}
|
|
2611
|
+
if ((lock.lockfileVersion ?? 1) < 2 || !lock.packages) return [];
|
|
2612
|
+
const packages = [];
|
|
2613
|
+
for (const [pkgPath, meta] of Object.entries(lock.packages)) {
|
|
2614
|
+
if (pkgPath === "") continue;
|
|
2615
|
+
if (!meta.version) continue;
|
|
2616
|
+
const isDev = meta.dev === true || meta.devOptional === true;
|
|
2617
|
+
if (isDev && !opts.includeDev) continue;
|
|
2618
|
+
const withoutRoot = pkgPath.startsWith("node_modules/") ? pkgPath.slice("node_modules/".length) : pkgPath;
|
|
2619
|
+
const segments = withoutRoot.split("/node_modules/");
|
|
2620
|
+
const isTransitive = segments.length > 1;
|
|
2621
|
+
if (isTransitive && !opts.includeTransitive) continue;
|
|
2622
|
+
const name = segments[segments.length - 1] ?? pkgPath;
|
|
2623
|
+
packages.push({
|
|
2624
|
+
name,
|
|
2625
|
+
version: meta.version,
|
|
2626
|
+
ecosystem: "npm",
|
|
2627
|
+
source: lockFilePath,
|
|
2628
|
+
type: isTransitive ? "transitive" : "direct",
|
|
2629
|
+
scope: isDev ? "dev" : "production"
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
return packages;
|
|
2633
|
+
}
|
|
2634
|
+
function parseYarnLock(content, lockFilePath, opts, packageJsonContent) {
|
|
2635
|
+
let pkgJson;
|
|
2636
|
+
try {
|
|
2637
|
+
pkgJson = JSON.parse(packageJsonContent);
|
|
2638
|
+
} catch {
|
|
2639
|
+
pkgJson = {};
|
|
2640
|
+
}
|
|
2641
|
+
const directDeps = new Set(Object.keys(pkgJson.dependencies ?? {}));
|
|
2642
|
+
const devDeps = new Set(Object.keys(pkgJson.devDependencies ?? {}));
|
|
2643
|
+
const allDirectDeps = /* @__PURE__ */ new Set([...directDeps, ...devDeps]);
|
|
2644
|
+
const packages = [];
|
|
2645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2646
|
+
const blockRegex = /^"?(@?[^@"\n][^"\n]*)@[^:]+:?\n\s+version "([^"]+)"/gm;
|
|
2647
|
+
for (const match of content.matchAll(blockRegex)) {
|
|
2648
|
+
const name = match[1].trim().replace(/^"|"$/g, "");
|
|
2649
|
+
const version = match[2];
|
|
2650
|
+
const key = `${name}@${version}`;
|
|
2651
|
+
if (seen.has(key)) continue;
|
|
2652
|
+
seen.add(key);
|
|
2653
|
+
const isDev = devDeps.has(name) && !directDeps.has(name);
|
|
2654
|
+
if (isDev && !opts.includeDev) continue;
|
|
2655
|
+
const isTransitive = !allDirectDeps.has(name);
|
|
2656
|
+
if (isTransitive && !opts.includeTransitive) continue;
|
|
2657
|
+
packages.push({
|
|
2658
|
+
name,
|
|
2659
|
+
version,
|
|
2660
|
+
ecosystem: "npm",
|
|
2661
|
+
source: lockFilePath,
|
|
2662
|
+
type: isTransitive ? "transitive" : "direct",
|
|
2663
|
+
scope: isDev ? "dev" : "production"
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
return packages;
|
|
2667
|
+
}
|
|
2668
|
+
function parsePnpmLock(content, lockFilePath, opts, packageJsonContent) {
|
|
2669
|
+
let pkgJson;
|
|
2670
|
+
try {
|
|
2671
|
+
pkgJson = JSON.parse(packageJsonContent);
|
|
2672
|
+
} catch {
|
|
2673
|
+
pkgJson = {};
|
|
2674
|
+
}
|
|
2675
|
+
const directDeps = new Set(Object.keys(pkgJson.dependencies ?? {}));
|
|
2676
|
+
const devDeps = new Set(Object.keys(pkgJson.devDependencies ?? {}));
|
|
2677
|
+
const allDirectDeps = /* @__PURE__ */ new Set([...directDeps, ...devDeps]);
|
|
2678
|
+
const packages = [];
|
|
2679
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2680
|
+
const blockRegex = /^\s{2}(\/?@?[^@\s/][^@\s]*)@(\d[^:\s]*):\s*\n((?:\s{4}[^\n]+\n)*)/gm;
|
|
2681
|
+
for (const match of content.matchAll(blockRegex)) {
|
|
2682
|
+
const name = match[1].replace(/^\//, "");
|
|
2683
|
+
const version = match[2];
|
|
2684
|
+
const attrs = match[3] ?? "";
|
|
2685
|
+
const key = `${name}@${version}`;
|
|
2686
|
+
if (seen.has(key)) continue;
|
|
2687
|
+
seen.add(key);
|
|
2688
|
+
const isDev = devDeps.has(name) && !directDeps.has(name) || /^\s+dev:\s*true/m.test(attrs);
|
|
2689
|
+
if (isDev && !opts.includeDev) continue;
|
|
2690
|
+
const isTransitive = !allDirectDeps.has(name);
|
|
2691
|
+
if (isTransitive && !opts.includeTransitive) continue;
|
|
2692
|
+
packages.push({
|
|
2693
|
+
name,
|
|
2694
|
+
version,
|
|
2695
|
+
ecosystem: "npm",
|
|
2696
|
+
source: lockFilePath,
|
|
2697
|
+
type: isTransitive ? "transitive" : "direct",
|
|
2698
|
+
scope: isDev ? "dev" : "production"
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
return packages;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// src/services/deps/parsers/nuget.ts
|
|
2705
|
+
import path3 from "path";
|
|
2706
|
+
var PROJ_EXTENSIONS = /* @__PURE__ */ new Set([".csproj", ".fsproj", ".vbproj"]);
|
|
2707
|
+
function findNugetManifests(files) {
|
|
2708
|
+
const lockFiles = /* @__PURE__ */ new Map();
|
|
2709
|
+
for (const f of files) {
|
|
2710
|
+
if (path3.basename(f) === "packages.lock.json") {
|
|
2711
|
+
lockFiles.set(path3.dirname(f), f);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const manifests = [];
|
|
2715
|
+
for (const file of files) {
|
|
2716
|
+
const base = path3.basename(file);
|
|
2717
|
+
const ext = path3.extname(file);
|
|
2718
|
+
if (PROJ_EXTENSIONS.has(ext) || base === "packages.config") {
|
|
2719
|
+
const dir = path3.dirname(file);
|
|
2720
|
+
const lockFile = lockFiles.get(dir);
|
|
2721
|
+
manifests.push({ file, ecosystem: "nuget", lockFile });
|
|
2722
|
+
}
|
|
2723
|
+
if (base === "Directory.Packages.props") {
|
|
2724
|
+
const dir = path3.dirname(file);
|
|
2725
|
+
manifests.push({ file, ecosystem: "nuget", lockFile: lockFiles.get(dir) });
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
const deduped = [];
|
|
2729
|
+
const seenLocks = /* @__PURE__ */ new Set();
|
|
2730
|
+
for (const manifest of manifests) {
|
|
2731
|
+
if (!manifest.lockFile) {
|
|
2732
|
+
deduped.push(manifest);
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
if (!seenLocks.has(manifest.lockFile)) {
|
|
2736
|
+
seenLocks.add(manifest.lockFile);
|
|
2737
|
+
deduped.push(manifest);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return deduped;
|
|
2741
|
+
}
|
|
2742
|
+
function parseNugetPackages(content, manifest, opts, lockContent, propsContent) {
|
|
2743
|
+
const base = path3.basename(manifest.file);
|
|
2744
|
+
if (lockContent) {
|
|
2745
|
+
return parsePackagesLock(lockContent, manifest.lockFile ?? manifest.file, opts);
|
|
2746
|
+
}
|
|
2747
|
+
if (base === "packages.config") {
|
|
2748
|
+
return parsePackagesConfig(content, manifest.file);
|
|
2749
|
+
}
|
|
2750
|
+
if (base === "Directory.Packages.props") {
|
|
2751
|
+
return parseDirectoryPackagesProps(content, manifest.file);
|
|
2752
|
+
}
|
|
2753
|
+
return parseCsproj(content, manifest.file, propsContent);
|
|
2754
|
+
}
|
|
2755
|
+
function parseCsproj(content, filePath, propsContent) {
|
|
2756
|
+
const centralVersions = propsContent ? extractCentralVersions(propsContent) : /* @__PURE__ */ new Map();
|
|
2757
|
+
const packages = [];
|
|
2758
|
+
const singleLine = /<PackageReference\s+([^>]+?)\/>/gi;
|
|
2759
|
+
const multiLine = /<PackageReference\s+([^>]+?)>([\s\S]*?)<\/PackageReference>/gi;
|
|
2760
|
+
const extractFromAttrsAndInner = (attrs, inner) => {
|
|
2761
|
+
const nameMatch = /\bInclude="([^"]+)"/i.exec(attrs);
|
|
2762
|
+
if (!nameMatch) return;
|
|
2763
|
+
const name = nameMatch[1];
|
|
2764
|
+
let version;
|
|
2765
|
+
const attrVersion = /\bVersion="([^"]+)"/i.exec(attrs);
|
|
2766
|
+
if (attrVersion) {
|
|
2767
|
+
version = attrVersion[1];
|
|
2768
|
+
} else if (inner) {
|
|
2769
|
+
const innerVersion = /<Version>([^<]+)<\/Version>/i.exec(inner);
|
|
2770
|
+
if (innerVersion) version = innerVersion[1];
|
|
2771
|
+
}
|
|
2772
|
+
if (!version || version.startsWith("$(")) {
|
|
2773
|
+
version = centralVersions.get(name) ?? centralVersions.get(name.toLowerCase());
|
|
2774
|
+
}
|
|
2775
|
+
if (!version) return;
|
|
2776
|
+
packages.push({
|
|
2777
|
+
name,
|
|
2778
|
+
version,
|
|
2779
|
+
ecosystem: "nuget",
|
|
2780
|
+
source: filePath,
|
|
2781
|
+
type: "direct",
|
|
2782
|
+
scope: "production"
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
for (const m of content.matchAll(singleLine)) {
|
|
2786
|
+
extractFromAttrsAndInner(m[1], "");
|
|
2787
|
+
}
|
|
2788
|
+
for (const m of content.matchAll(multiLine)) {
|
|
2789
|
+
extractFromAttrsAndInner(m[1], m[2] ?? "");
|
|
2790
|
+
}
|
|
2791
|
+
return packages;
|
|
2792
|
+
}
|
|
2793
|
+
function extractCentralVersions(propsContent) {
|
|
2794
|
+
const map = /* @__PURE__ */ new Map();
|
|
2795
|
+
const regex = /<PackageVersion\s+Include="([^"]+)"\s+Version="([^"]+)"/gi;
|
|
2796
|
+
for (const m of propsContent.matchAll(regex)) {
|
|
2797
|
+
map.set(m[1], m[2]);
|
|
2798
|
+
map.set(m[1].toLowerCase(), m[2]);
|
|
2799
|
+
}
|
|
2800
|
+
return map;
|
|
2801
|
+
}
|
|
2802
|
+
function parseDirectoryPackagesProps(content, filePath) {
|
|
2803
|
+
const packages = [];
|
|
2804
|
+
const regex = /<PackageVersion\s+Include="([^"]+)"\s+Version="([^"]+)"/gi;
|
|
2805
|
+
for (const m of content.matchAll(regex)) {
|
|
2806
|
+
packages.push({
|
|
2807
|
+
name: m[1],
|
|
2808
|
+
version: m[2],
|
|
2809
|
+
ecosystem: "nuget",
|
|
2810
|
+
source: filePath,
|
|
2811
|
+
type: "direct",
|
|
2812
|
+
scope: "production"
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
return packages;
|
|
2816
|
+
}
|
|
2817
|
+
function parsePackagesConfig(content, filePath) {
|
|
2818
|
+
const packages = [];
|
|
2819
|
+
const regex = /<package\s+id="([^"]+)"\s+version="([^"]+)"/gi;
|
|
2820
|
+
for (const m of content.matchAll(regex)) {
|
|
2821
|
+
packages.push({
|
|
2822
|
+
name: m[1],
|
|
2823
|
+
version: m[2],
|
|
2824
|
+
ecosystem: "nuget",
|
|
2825
|
+
source: filePath,
|
|
2826
|
+
type: "direct",
|
|
2827
|
+
scope: "production"
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
return packages;
|
|
2831
|
+
}
|
|
2832
|
+
function parsePackagesLock(content, filePath, opts) {
|
|
2833
|
+
let lock;
|
|
2834
|
+
try {
|
|
2835
|
+
lock = JSON.parse(content);
|
|
2836
|
+
} catch {
|
|
2837
|
+
return [];
|
|
2838
|
+
}
|
|
2839
|
+
const packages = [];
|
|
2840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2841
|
+
for (const frameworkDeps of Object.values(lock.dependencies ?? {})) {
|
|
2842
|
+
for (const [name, meta] of Object.entries(frameworkDeps)) {
|
|
2843
|
+
const version = meta.resolved ?? "";
|
|
2844
|
+
if (!version) continue;
|
|
2845
|
+
const isTransitive = meta.type === "Transitive";
|
|
2846
|
+
if (isTransitive && !opts.includeTransitive) continue;
|
|
2847
|
+
const key = `${name}@${version}`;
|
|
2848
|
+
if (seen.has(key)) continue;
|
|
2849
|
+
seen.add(key);
|
|
2850
|
+
packages.push({
|
|
2851
|
+
name,
|
|
2852
|
+
version,
|
|
2853
|
+
ecosystem: "nuget",
|
|
2854
|
+
source: filePath,
|
|
2855
|
+
type: isTransitive ? "transitive" : "direct",
|
|
2856
|
+
scope: "production"
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
return packages;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/services/deps/parsers/maven.ts
|
|
2864
|
+
import path4 from "path";
|
|
2865
|
+
function findMavenManifests(files) {
|
|
2866
|
+
const lockFiles = new Set(files.filter((f) => path4.basename(f) === "gradle.lockfile"));
|
|
2867
|
+
const manifests = [];
|
|
2868
|
+
for (const file of files) {
|
|
2869
|
+
const base = path4.basename(file);
|
|
2870
|
+
if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") {
|
|
2871
|
+
const dir = path4.dirname(file);
|
|
2872
|
+
const lockCandidate = dir === "." ? "gradle.lockfile" : `${dir}/gradle.lockfile`;
|
|
2873
|
+
const lockFile = lockFiles.has(lockCandidate) ? lockCandidate : void 0;
|
|
2874
|
+
manifests.push({ file, ecosystem: "maven", lockFile });
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return manifests;
|
|
2878
|
+
}
|
|
2879
|
+
function parseMavenPackages(content, manifest, opts, lockContent) {
|
|
2880
|
+
const base = path4.basename(manifest.file);
|
|
2881
|
+
if (lockContent && manifest.lockFile) {
|
|
2882
|
+
return parseGradleLock(lockContent, manifest.lockFile, opts);
|
|
2883
|
+
}
|
|
2884
|
+
if (base === "pom.xml") {
|
|
2885
|
+
return parsePomXml(content, manifest.file, opts);
|
|
2886
|
+
}
|
|
2887
|
+
if (base === "build.gradle" || base === "build.gradle.kts") {
|
|
2888
|
+
return parseBuildGradle(content, manifest.file, opts);
|
|
2889
|
+
}
|
|
2890
|
+
return [];
|
|
2891
|
+
}
|
|
2892
|
+
function parsePomXml(content, filePath, opts) {
|
|
2893
|
+
const props = extractPomProperties(content);
|
|
2894
|
+
const managedVersions = extractDependencyManagement(content, props);
|
|
2895
|
+
const withoutMgmt = content.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/gi, "");
|
|
2896
|
+
const packages = [];
|
|
2897
|
+
const depRegex = /<dependency>([\s\S]*?)<\/dependency>/gi;
|
|
2898
|
+
for (const match of withoutMgmt.matchAll(depRegex)) {
|
|
2899
|
+
const inner = match[1];
|
|
2900
|
+
const groupId = /<groupId>\s*([^<\s]+)\s*<\/groupId>/i.exec(inner)?.[1];
|
|
2901
|
+
const artifactId = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/i.exec(inner)?.[1];
|
|
2902
|
+
if (!groupId || !artifactId) continue;
|
|
2903
|
+
const rawVersion = /<version>\s*([^<\s]+)\s*<\/version>/i.exec(inner)?.[1];
|
|
2904
|
+
const resolvedVersion = resolveProperty(rawVersion ?? null, props) ?? managedVersions.get(`${groupId}:${artifactId}`);
|
|
2905
|
+
if (!resolvedVersion) continue;
|
|
2906
|
+
const scope = /<scope>\s*([^<\s]+)\s*<\/scope>/i.exec(inner)?.[1] ?? "compile";
|
|
2907
|
+
const isDev = scope === "test" || scope === "provided";
|
|
2908
|
+
if (isDev && !opts.includeDev) continue;
|
|
2909
|
+
packages.push({
|
|
2910
|
+
name: `${groupId}:${artifactId}`,
|
|
2911
|
+
version: resolvedVersion,
|
|
2912
|
+
ecosystem: "maven",
|
|
2913
|
+
source: filePath,
|
|
2914
|
+
type: "direct",
|
|
2915
|
+
scope: isDev ? "dev" : "production"
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return packages;
|
|
2919
|
+
}
|
|
2920
|
+
function extractPomProperties(content) {
|
|
2921
|
+
const props = /* @__PURE__ */ new Map();
|
|
2922
|
+
const propsMatch = /<properties>([\s\S]*?)<\/properties>/i.exec(content);
|
|
2923
|
+
if (!propsMatch) return props;
|
|
2924
|
+
const propRegex = /<([a-zA-Z0-9._-]+)>\s*([^<]+)\s*<\/\1>/g;
|
|
2925
|
+
for (const m of propsMatch[1].matchAll(propRegex)) {
|
|
2926
|
+
props.set(m[1], m[2].trim());
|
|
2927
|
+
}
|
|
2928
|
+
return props;
|
|
2929
|
+
}
|
|
2930
|
+
function extractDependencyManagement(content, props) {
|
|
2931
|
+
const versions = /* @__PURE__ */ new Map();
|
|
2932
|
+
const mgmtMatch = /<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/i.exec(content);
|
|
2933
|
+
if (!mgmtMatch) return versions;
|
|
2934
|
+
const depRegex = /<dependency>([\s\S]*?)<\/dependency>/gi;
|
|
2935
|
+
for (const m of mgmtMatch[1].matchAll(depRegex)) {
|
|
2936
|
+
const inner = m[1];
|
|
2937
|
+
const groupId = /<groupId>\s*([^<\s]+)\s*<\/groupId>/i.exec(inner)?.[1];
|
|
2938
|
+
const artifactId = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/i.exec(inner)?.[1];
|
|
2939
|
+
const rawVersion = /<version>\s*([^<\s]+)\s*<\/version>/i.exec(inner)?.[1];
|
|
2940
|
+
if (!groupId || !artifactId || !rawVersion) continue;
|
|
2941
|
+
const resolved = resolveProperty(rawVersion, props);
|
|
2942
|
+
if (resolved) versions.set(`${groupId}:${artifactId}`, resolved);
|
|
2943
|
+
}
|
|
2944
|
+
return versions;
|
|
2945
|
+
}
|
|
2946
|
+
function resolveProperty(value, props) {
|
|
2947
|
+
if (!value) return null;
|
|
2948
|
+
const resolved = value.replace(/\$\{([^}]+)\}/g, (_, key) => props.get(key) ?? `\${${key}}`);
|
|
2949
|
+
return /\$\{[^}]+\}/.test(resolved) ? null : resolved;
|
|
2950
|
+
}
|
|
2951
|
+
var GRADLE_DEV_CONFIGS = /* @__PURE__ */ new Set(["testImplementation", "testCompileOnly", "testRuntimeOnly", "testApi"]);
|
|
2952
|
+
function parseBuildGradle(content, filePath, opts) {
|
|
2953
|
+
const packages = [];
|
|
2954
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2955
|
+
const stringNotation = /\b(\w+)\s*\(?\s*['"]([^:'"]+):([^:'"]+):([^'"]+)['"]\s*\)?/g;
|
|
2956
|
+
for (const m of content.matchAll(stringNotation)) {
|
|
2957
|
+
const config = m[1];
|
|
2958
|
+
const groupId = m[2];
|
|
2959
|
+
const artifactId = m[3];
|
|
2960
|
+
const version = m[4].split("@")[0];
|
|
2961
|
+
const isDev = GRADLE_DEV_CONFIGS.has(config);
|
|
2962
|
+
if (isDev && !opts.includeDev) continue;
|
|
2963
|
+
const name = `${groupId}:${artifactId}`;
|
|
2964
|
+
const key = `${name}@${version}`;
|
|
2965
|
+
if (seen.has(key)) continue;
|
|
2966
|
+
seen.add(key);
|
|
2967
|
+
packages.push({
|
|
2968
|
+
name,
|
|
2969
|
+
version,
|
|
2970
|
+
ecosystem: "maven",
|
|
2971
|
+
source: filePath,
|
|
2972
|
+
type: "direct",
|
|
2973
|
+
scope: isDev ? "dev" : "production"
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
const mapNotation = /\b(\w+)\s+group:\s*['"]([^'"]+)['"]\s*,\s*name:\s*['"]([^'"]+)['"]\s*,\s*version:\s*['"]([^'"]+)['"]/g;
|
|
2977
|
+
for (const m of content.matchAll(mapNotation)) {
|
|
2978
|
+
const config = m[1];
|
|
2979
|
+
const groupId = m[2];
|
|
2980
|
+
const artifactId = m[3];
|
|
2981
|
+
const version = m[4];
|
|
2982
|
+
const isDev = GRADLE_DEV_CONFIGS.has(config);
|
|
2983
|
+
if (isDev && !opts.includeDev) continue;
|
|
2984
|
+
const name = `${groupId}:${artifactId}`;
|
|
2985
|
+
const key = `${name}@${version}`;
|
|
2986
|
+
if (seen.has(key)) continue;
|
|
2987
|
+
seen.add(key);
|
|
2988
|
+
packages.push({
|
|
2989
|
+
name,
|
|
2990
|
+
version,
|
|
2991
|
+
ecosystem: "maven",
|
|
2992
|
+
source: filePath,
|
|
2993
|
+
type: "direct",
|
|
2994
|
+
scope: isDev ? "dev" : "production"
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
return packages;
|
|
2998
|
+
}
|
|
2999
|
+
function parseGradleLock(content, filePath, opts) {
|
|
3000
|
+
const packages = [];
|
|
3001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3002
|
+
const lineRegex = /^([^:#\s]+):([^:#\s]+):([^=\s]+)=(.+)$/gm;
|
|
3003
|
+
for (const m of content.matchAll(lineRegex)) {
|
|
3004
|
+
const groupId = m[1];
|
|
3005
|
+
const artifactId = m[2];
|
|
3006
|
+
const version = m[3];
|
|
3007
|
+
const configs = m[4].split(",").map((s) => s.trim());
|
|
3008
|
+
const isDev = configs.every((c) => GRADLE_DEV_CONFIGS.has(c));
|
|
3009
|
+
if (isDev && !opts.includeDev) continue;
|
|
3010
|
+
const name = `${groupId}:${artifactId}`;
|
|
3011
|
+
const key = `${name}@${version}`;
|
|
3012
|
+
if (seen.has(key)) continue;
|
|
3013
|
+
seen.add(key);
|
|
3014
|
+
packages.push({
|
|
3015
|
+
name,
|
|
3016
|
+
version,
|
|
3017
|
+
ecosystem: "maven",
|
|
3018
|
+
source: filePath,
|
|
3019
|
+
type: "direct",
|
|
3020
|
+
scope: isDev ? "dev" : "production"
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
return packages;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// src/services/deps/parsers/index.ts
|
|
3027
|
+
function getRepoFiles(repoRoot) {
|
|
3028
|
+
try {
|
|
3029
|
+
const out = execSync4("git ls-files --cached --others --exclude-standard", {
|
|
3030
|
+
encoding: "utf8",
|
|
3031
|
+
cwd: repoRoot,
|
|
3032
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3033
|
+
});
|
|
3034
|
+
return out.trim().split("\n").filter(Boolean);
|
|
3035
|
+
} catch {
|
|
3036
|
+
return [];
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
function readFile(repoRoot, relPath) {
|
|
3040
|
+
try {
|
|
3041
|
+
return fs4.readFileSync(path5.join(repoRoot, relPath), "utf8");
|
|
3042
|
+
} catch {
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
function readFileAtRef(repoRoot, ref, relPath) {
|
|
3047
|
+
try {
|
|
3048
|
+
return execFileSync2("git", ["show", `${ref}:${relPath}`], {
|
|
3049
|
+
encoding: "utf8",
|
|
3050
|
+
cwd: repoRoot,
|
|
3051
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3052
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3053
|
+
});
|
|
3054
|
+
} catch {
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
function findPropsContent(manifestFile, readFn) {
|
|
3059
|
+
let dir = path5.dirname(manifestFile);
|
|
3060
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
3061
|
+
const candidate = dir === "." ? "Directory.Packages.props" : `${dir}/Directory.Packages.props`;
|
|
3062
|
+
const content = readFn(candidate);
|
|
3063
|
+
if (content) return content;
|
|
3064
|
+
if (dir === ".") break;
|
|
3065
|
+
dir = path5.dirname(dir);
|
|
3066
|
+
}
|
|
3067
|
+
return null;
|
|
3068
|
+
}
|
|
3069
|
+
function parseManifests(manifests, opts, readFn) {
|
|
3070
|
+
const allPackages = [];
|
|
3071
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3072
|
+
for (const manifest of manifests) {
|
|
3073
|
+
const content = readFn(manifest.file);
|
|
3074
|
+
if (!content) continue;
|
|
3075
|
+
const lockContent = manifest.lockFile ? readFn(manifest.lockFile) ?? void 0 : void 0;
|
|
3076
|
+
let pkgs = [];
|
|
3077
|
+
if (manifest.ecosystem === "npm") {
|
|
3078
|
+
pkgs = parseNpmPackages(content, manifest, opts, lockContent);
|
|
3079
|
+
} else if (manifest.ecosystem === "nuget") {
|
|
3080
|
+
const propsContent = findPropsContent(manifest.file, readFn) ?? void 0;
|
|
3081
|
+
pkgs = parseNugetPackages(content, manifest, opts, lockContent, propsContent);
|
|
3082
|
+
} else if (manifest.ecosystem === "maven") {
|
|
3083
|
+
pkgs = parseMavenPackages(content, manifest, opts, lockContent);
|
|
3084
|
+
}
|
|
3085
|
+
for (const pkg2 of pkgs) {
|
|
3086
|
+
const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}:${pkg2.source}`;
|
|
3087
|
+
if (!seen.has(key)) {
|
|
3088
|
+
seen.add(key);
|
|
3089
|
+
allPackages.push(pkg2);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return allPackages;
|
|
3094
|
+
}
|
|
3095
|
+
function scanRepo(repoRoot, opts = {}) {
|
|
3096
|
+
const files = getRepoFiles(repoRoot);
|
|
3097
|
+
const eco = opts.ecosystem ?? "all";
|
|
3098
|
+
const npmManifests = eco === "all" || eco === "npm" ? findNpmManifests(files) : [];
|
|
3099
|
+
const nugetManifests = eco === "all" || eco === "nuget" ? findNugetManifests(files) : [];
|
|
3100
|
+
const mavenManifests = eco === "all" || eco === "maven" ? findMavenManifests(files) : [];
|
|
3101
|
+
const manifests = [...npmManifests, ...nugetManifests, ...mavenManifests];
|
|
3102
|
+
const packages = parseManifests(manifests, opts, (rel) => readFile(repoRoot, rel));
|
|
3103
|
+
return buildScanData(manifests, packages);
|
|
3104
|
+
}
|
|
3105
|
+
function scanRepoAtRef(repoRoot, ref, opts = {}) {
|
|
3106
|
+
const eco = opts.ecosystem ?? "all";
|
|
3107
|
+
let files = [];
|
|
3108
|
+
try {
|
|
3109
|
+
const out = execFileSync2("git", ["ls-tree", "-r", "--name-only", ref], {
|
|
3110
|
+
encoding: "utf8",
|
|
3111
|
+
cwd: repoRoot,
|
|
3112
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3113
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3114
|
+
});
|
|
3115
|
+
files = out.trim().split("\n").filter(Boolean);
|
|
3116
|
+
} catch {
|
|
3117
|
+
return buildScanData([], []);
|
|
3118
|
+
}
|
|
3119
|
+
const npmManifests = eco === "all" || eco === "npm" ? findNpmManifests(files) : [];
|
|
3120
|
+
const nugetManifests = eco === "all" || eco === "nuget" ? findNugetManifests(files) : [];
|
|
3121
|
+
const mavenManifests = eco === "all" || eco === "maven" ? findMavenManifests(files) : [];
|
|
3122
|
+
const manifests = [...npmManifests, ...nugetManifests, ...mavenManifests];
|
|
3123
|
+
const packages = parseManifests(manifests, opts, (rel) => readFileAtRef(repoRoot, ref, rel));
|
|
3124
|
+
return buildScanData(manifests, packages);
|
|
3125
|
+
}
|
|
3126
|
+
function buildScanData(manifests, packages) {
|
|
3127
|
+
const ecosystemSet = new Set(manifests.map((m) => m.ecosystem));
|
|
3128
|
+
const byEcosystem = {};
|
|
3129
|
+
const byType = { direct: 0, transitive: 0 };
|
|
3130
|
+
const byScope = { production: 0, dev: 0 };
|
|
3131
|
+
for (const pkg2 of packages) {
|
|
3132
|
+
byEcosystem[pkg2.ecosystem] = (byEcosystem[pkg2.ecosystem] ?? 0) + 1;
|
|
3133
|
+
byType[pkg2.type] = (byType[pkg2.type] ?? 0) + 1;
|
|
3134
|
+
byScope[pkg2.scope] = (byScope[pkg2.scope] ?? 0) + 1;
|
|
3135
|
+
}
|
|
3136
|
+
return {
|
|
3137
|
+
ecosystems: Array.from(ecosystemSet),
|
|
3138
|
+
manifests,
|
|
3139
|
+
packages,
|
|
3140
|
+
summary: {
|
|
3141
|
+
totalPackages: packages.length,
|
|
3142
|
+
byEcosystem,
|
|
3143
|
+
byType,
|
|
3144
|
+
byScope
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/services/deps/scan.ts
|
|
3150
|
+
function runScan(config, opts) {
|
|
3151
|
+
void config;
|
|
3152
|
+
const repoRoot = getRepoRoot2();
|
|
3153
|
+
if (!repoRoot) {
|
|
3154
|
+
throw new PncliError("Not inside a git repository. deps commands require a git repo root.", 1);
|
|
3155
|
+
}
|
|
3156
|
+
return scanRepo(repoRoot, opts);
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// src/services/deps/semver.ts
|
|
3160
|
+
function parse(v) {
|
|
3161
|
+
const clean = v.replace(/^[^0-9]*/, "");
|
|
3162
|
+
const dashIdx = clean.indexOf("-");
|
|
3163
|
+
const plusIdx = clean.indexOf("+");
|
|
3164
|
+
const metaStart = plusIdx > -1 ? plusIdx : Infinity;
|
|
3165
|
+
const preStart = dashIdx > -1 && dashIdx < metaStart ? dashIdx : Infinity;
|
|
3166
|
+
const numeric = clean.slice(0, Math.min(preStart, metaStart));
|
|
3167
|
+
const preRelease = preStart < Infinity ? clean.slice(preStart + 1, metaStart < Infinity ? metaStart : void 0) : "";
|
|
3168
|
+
const parts = numeric.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3169
|
+
return { major: parts[0] ?? 0, minor: parts[1] ?? 0, patch: parts[2] ?? 0, preRelease };
|
|
3170
|
+
}
|
|
3171
|
+
function compareSemver(a, b) {
|
|
3172
|
+
const av = parse(a);
|
|
3173
|
+
const bv = parse(b);
|
|
3174
|
+
if (av.major !== bv.major) return av.major - bv.major;
|
|
3175
|
+
if (av.minor !== bv.minor) return av.minor - bv.minor;
|
|
3176
|
+
if (av.patch !== bv.patch) return av.patch - bv.patch;
|
|
3177
|
+
if (!av.preRelease && bv.preRelease) return 1;
|
|
3178
|
+
if (av.preRelease && !bv.preRelease) return -1;
|
|
3179
|
+
if (av.preRelease && bv.preRelease) return av.preRelease.localeCompare(bv.preRelease);
|
|
3180
|
+
return 0;
|
|
3181
|
+
}
|
|
3182
|
+
function isNewer(current, latest) {
|
|
3183
|
+
return compareSemver(latest, current) > 0;
|
|
3184
|
+
}
|
|
3185
|
+
function updateType(current, latest) {
|
|
3186
|
+
const cv = parse(current);
|
|
3187
|
+
const lv = parse(latest);
|
|
3188
|
+
if (lv.major > cv.major) return "major";
|
|
3189
|
+
if (lv.minor > cv.minor) return "minor";
|
|
3190
|
+
return "patch";
|
|
3191
|
+
}
|
|
3192
|
+
function isDowngrade(from, to) {
|
|
3193
|
+
return compareSemver(to, from) < 0;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/services/deps/diff.ts
|
|
3197
|
+
function runDiff(config, from, to, opts) {
|
|
3198
|
+
void config;
|
|
3199
|
+
const repoRoot = getRepoRoot2();
|
|
3200
|
+
if (!repoRoot) {
|
|
3201
|
+
throw new PncliError("Not inside a git repository.", 1);
|
|
3202
|
+
}
|
|
3203
|
+
const fromScan = scanRepoAtRef(repoRoot, from, opts);
|
|
3204
|
+
const toScan = to ? scanRepoAtRef(repoRoot, to, opts) : scanRepo(repoRoot, opts);
|
|
3205
|
+
const fromMap = /* @__PURE__ */ new Map();
|
|
3206
|
+
const toMap = /* @__PURE__ */ new Map();
|
|
3207
|
+
for (const pkg2 of fromScan.packages) {
|
|
3208
|
+
const key = `${pkg2.ecosystem}:${pkg2.name}:${pkg2.source}`;
|
|
3209
|
+
fromMap.set(key, pkg2.version);
|
|
3210
|
+
}
|
|
3211
|
+
for (const pkg2 of toScan.packages) {
|
|
3212
|
+
const key = `${pkg2.ecosystem}:${pkg2.name}:${pkg2.source}`;
|
|
3213
|
+
toMap.set(key, pkg2.version);
|
|
3214
|
+
}
|
|
3215
|
+
const changes = [];
|
|
3216
|
+
const allKeys = /* @__PURE__ */ new Set([...fromMap.keys(), ...toMap.keys()]);
|
|
3217
|
+
for (const key of allKeys) {
|
|
3218
|
+
const parts = key.split(":");
|
|
3219
|
+
const eco = parts[0];
|
|
3220
|
+
const source = parts[parts.length - 1] ?? "";
|
|
3221
|
+
const name = parts.slice(1, -1).join(":");
|
|
3222
|
+
const fromVer = fromMap.get(key) ?? null;
|
|
3223
|
+
const toVer = toMap.get(key) ?? null;
|
|
3224
|
+
let change;
|
|
3225
|
+
if (!fromVer) {
|
|
3226
|
+
change = "added";
|
|
3227
|
+
} else if (!toVer) {
|
|
3228
|
+
change = "removed";
|
|
3229
|
+
} else if (fromVer === toVer) {
|
|
3230
|
+
continue;
|
|
3231
|
+
} else {
|
|
3232
|
+
change = isDowngrade(fromVer, toVer) ? "downgraded" : "upgraded";
|
|
3233
|
+
}
|
|
3234
|
+
changes.push({ name, ecosystem: eco, change, from: fromVer, to: toVer, source });
|
|
3235
|
+
}
|
|
3236
|
+
const summary = { added: 0, removed: 0, upgraded: 0, downgraded: 0, unchanged: 0 };
|
|
3237
|
+
for (const c of changes) summary[c.change]++;
|
|
3238
|
+
summary.unchanged = [...allKeys].filter((k) => {
|
|
3239
|
+
const fv = fromMap.get(k);
|
|
3240
|
+
const tv = toMap.get(k);
|
|
3241
|
+
return fv !== void 0 && tv !== void 0 && fv === tv;
|
|
3242
|
+
}).length;
|
|
3243
|
+
return { from, to: to ?? "working tree", changes, summary };
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// src/services/deps/clients/osv.ts
|
|
3247
|
+
var OSV_URL = "https://api.osv.dev";
|
|
3248
|
+
var BATCH_SIZE = 500;
|
|
3249
|
+
var TIMEOUT_MS = 3e4;
|
|
3250
|
+
var OSV_ECOSYSTEM = {
|
|
3251
|
+
npm: "npm",
|
|
3252
|
+
nuget: "NuGet",
|
|
3253
|
+
maven: "Maven"
|
|
3254
|
+
};
|
|
3255
|
+
async function fetchWithTimeout2(url, init) {
|
|
3256
|
+
const controller = new AbortController();
|
|
3257
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
3258
|
+
try {
|
|
3259
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
3260
|
+
} finally {
|
|
3261
|
+
clearTimeout(timer);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
async function checkOsvConnectivity() {
|
|
3265
|
+
try {
|
|
3266
|
+
const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
|
|
3267
|
+
method: "POST",
|
|
3268
|
+
headers: { "Content-Type": "application/json" },
|
|
3269
|
+
body: JSON.stringify({ queries: [] })
|
|
3270
|
+
});
|
|
3271
|
+
return { reachable: res.ok || res.status === 400 };
|
|
3272
|
+
} catch (err) {
|
|
3273
|
+
return {
|
|
3274
|
+
reachable: false,
|
|
3275
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
function extractRemediation(vuln, pkg2) {
|
|
3280
|
+
const fixedVersions = [];
|
|
3281
|
+
for (const affected of vuln.affected ?? []) {
|
|
3282
|
+
for (const range of affected.ranges ?? []) {
|
|
3283
|
+
if (range.type === "SEMVER" || range.type === "ECOSYSTEM") {
|
|
3284
|
+
for (const event of range.events ?? []) {
|
|
3285
|
+
if (event.fixed) fixedVersions.push(event.fixed);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
const unique = [...new Set(fixedVersions)];
|
|
3291
|
+
const fixAvailable = unique.length > 0;
|
|
3292
|
+
const advice = fixAvailable ? `Upgrade ${pkg2.name} to ${unique.join(" or ")}` : `No fix available yet for ${pkg2.name}. Monitor ${vuln.id} for updates.`;
|
|
3293
|
+
return { fixAvailable, fixedVersions: unique, advice };
|
|
3294
|
+
}
|
|
3295
|
+
function extractSeverity(vuln) {
|
|
3296
|
+
const dbSpecific = vuln.database_specific ?? {};
|
|
3297
|
+
const dbSeverity = typeof dbSpecific["severity"] === "string" ? dbSpecific["severity"] : void 0;
|
|
3298
|
+
let cvss = null;
|
|
3299
|
+
const rawCvss = dbSpecific["cvss"];
|
|
3300
|
+
if (typeof rawCvss === "number") {
|
|
3301
|
+
cvss = rawCvss;
|
|
3302
|
+
} else if (rawCvss && typeof rawCvss === "object" && "score" in rawCvss) {
|
|
3303
|
+
const score = rawCvss["score"];
|
|
3304
|
+
if (typeof score === "number") cvss = score;
|
|
3305
|
+
}
|
|
3306
|
+
for (const s of vuln.severity ?? []) {
|
|
3307
|
+
const score = parseFloat(s.score);
|
|
3308
|
+
if (!isNaN(score) && score >= 0 && score <= 10) {
|
|
3309
|
+
cvss = score;
|
|
3310
|
+
break;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
severity: dbSeverity ?? inferSeverityFromCvss(cvss),
|
|
3315
|
+
cvss
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function inferSeverityFromCvss(cvss) {
|
|
3319
|
+
if (cvss === null) return "UNKNOWN";
|
|
3320
|
+
if (cvss >= 9) return "CRITICAL";
|
|
3321
|
+
if (cvss >= 7) return "HIGH";
|
|
3322
|
+
if (cvss >= 4) return "MEDIUM";
|
|
3323
|
+
return "LOW";
|
|
3324
|
+
}
|
|
3325
|
+
function mapVuln(vuln, pkg2) {
|
|
3326
|
+
const { severity, cvss } = extractSeverity(vuln);
|
|
3327
|
+
return {
|
|
3328
|
+
id: vuln.id,
|
|
3329
|
+
summary: vuln.summary ?? "",
|
|
3330
|
+
severity,
|
|
3331
|
+
cvss,
|
|
3332
|
+
aliases: vuln.aliases ?? [],
|
|
3333
|
+
remediation: extractRemediation(vuln, pkg2),
|
|
3334
|
+
references: (vuln.references ?? []).map((r) => r.url ?? "").filter(Boolean)
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
async function queryBatch(queries, packages) {
|
|
3338
|
+
const res = await fetchWithTimeout2(`${OSV_URL}/v1/querybatch`, {
|
|
3339
|
+
method: "POST",
|
|
3340
|
+
headers: { "Content-Type": "application/json" },
|
|
3341
|
+
body: JSON.stringify({ queries })
|
|
3342
|
+
});
|
|
3343
|
+
if (!res.ok) {
|
|
3344
|
+
throw new Error(`OSV.dev returned HTTP ${res.status}`);
|
|
3345
|
+
}
|
|
3346
|
+
const data = await res.json();
|
|
3347
|
+
const vulnerable = [];
|
|
3348
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
3349
|
+
const result = data.results[i];
|
|
3350
|
+
const pkg2 = packages[i];
|
|
3351
|
+
if (!result || !pkg2) continue;
|
|
3352
|
+
const vulns = result.vulns ?? [];
|
|
3353
|
+
if (vulns.length === 0) continue;
|
|
3354
|
+
vulnerable.push({
|
|
3355
|
+
...pkg2,
|
|
3356
|
+
vulnerabilities: vulns.map((v) => mapVuln(v, pkg2))
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
return vulnerable;
|
|
3360
|
+
}
|
|
3361
|
+
async function checkPackagesForVulns(packages) {
|
|
3362
|
+
const allVulnerable = [];
|
|
3363
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
3364
|
+
const chunk = packages.slice(i, i + BATCH_SIZE);
|
|
3365
|
+
const queries = chunk.map((pkg2) => ({
|
|
3366
|
+
package: {
|
|
3367
|
+
name: pkg2.name,
|
|
3368
|
+
ecosystem: OSV_ECOSYSTEM[pkg2.ecosystem] ?? pkg2.ecosystem
|
|
3369
|
+
},
|
|
3370
|
+
version: pkg2.version
|
|
3371
|
+
}));
|
|
3372
|
+
const results = await queryBatch(queries, chunk);
|
|
3373
|
+
allVulnerable.push(...results);
|
|
3374
|
+
}
|
|
3375
|
+
return allVulnerable;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// src/services/deps/clients/artifactory.ts
|
|
3379
|
+
var TIMEOUT_MS2 = 15e3;
|
|
3380
|
+
async function fetchWithTimeout3(url, token) {
|
|
3381
|
+
const controller = new AbortController();
|
|
3382
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
|
|
3383
|
+
try {
|
|
3384
|
+
return await fetch(url, {
|
|
3385
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
3386
|
+
signal: controller.signal
|
|
3387
|
+
});
|
|
3388
|
+
} finally {
|
|
3389
|
+
clearTimeout(timer);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
async function checkArtifactoryConnectivity(config) {
|
|
3393
|
+
if (!config.baseUrl && !config.token) {
|
|
3394
|
+
return {
|
|
3395
|
+
reachable: false,
|
|
3396
|
+
authenticated: false,
|
|
3397
|
+
configured: false,
|
|
3398
|
+
error: "Artifactory is not configured"
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
if (!config.baseUrl) {
|
|
3402
|
+
return {
|
|
3403
|
+
reachable: false,
|
|
3404
|
+
authenticated: false,
|
|
3405
|
+
configured: false,
|
|
3406
|
+
error: "Artifactory baseUrl is not set"
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
if (!config.token) {
|
|
3410
|
+
return {
|
|
3411
|
+
reachable: false,
|
|
3412
|
+
authenticated: false,
|
|
3413
|
+
configured: false,
|
|
3414
|
+
error: "Artifactory token is not set"
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const res = await fetchWithTimeout3(`${config.baseUrl}/api/system/ping`, config.token);
|
|
3419
|
+
if (res.status === 401 || res.status === 403) {
|
|
3420
|
+
return {
|
|
3421
|
+
reachable: true,
|
|
3422
|
+
authenticated: false,
|
|
3423
|
+
configured: true,
|
|
3424
|
+
error: "Artifactory token is invalid or lacks permissions"
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
return { reachable: res.ok, authenticated: res.ok, configured: true };
|
|
3428
|
+
} catch (err) {
|
|
3429
|
+
return {
|
|
3430
|
+
reachable: false,
|
|
3431
|
+
authenticated: false,
|
|
3432
|
+
configured: true,
|
|
3433
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
function requireArtifactory(config, command) {
|
|
3438
|
+
if (!config.baseUrl && !config.token) {
|
|
3439
|
+
throw new PncliError(
|
|
3440
|
+
`${command} requires Artifactory. Add the following to ~/.pncli/config.json:
|
|
3441
|
+
"artifactory": { "baseUrl": "https://artifactory.company.com", "token": "your-token" }
|
|
3442
|
+
Or set PNCLI_ARTIFACTORY_BASE_URL and PNCLI_ARTIFACTORY_TOKEN environment variables.`,
|
|
3443
|
+
503
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
if (!config.baseUrl) {
|
|
3447
|
+
throw new PncliError(
|
|
3448
|
+
`${command} requires an Artifactory URL. Set artifactory.baseUrl in ~/.pncli/config.json or PNCLI_ARTIFACTORY_BASE_URL.`,
|
|
3449
|
+
503
|
|
3450
|
+
);
|
|
3451
|
+
}
|
|
3452
|
+
if (!config.token) {
|
|
3453
|
+
throw new PncliError(
|
|
3454
|
+
`${command} requires an Artifactory token. Set artifactory.token in ~/.pncli/config.json or PNCLI_ARTIFACTORY_TOKEN.`,
|
|
3455
|
+
503
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function repoForEcosystem(config, eco) {
|
|
3460
|
+
if (eco === "npm") return config.npmRepo;
|
|
3461
|
+
if (eco === "nuget") return config.nugetRepo;
|
|
3462
|
+
if (eco === "maven") return config.mavenRepo;
|
|
3463
|
+
return void 0;
|
|
3464
|
+
}
|
|
3465
|
+
async function getLatestNpm(baseUrl, token, repoName, packageName) {
|
|
3466
|
+
try {
|
|
3467
|
+
const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
|
|
3468
|
+
const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
|
|
3469
|
+
if (!res.ok) return null;
|
|
3470
|
+
const data = await res.json();
|
|
3471
|
+
return data["dist-tags"]?.latest ?? null;
|
|
3472
|
+
} catch {
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
async function getLatestNuget(baseUrl, token, repoName, packageName) {
|
|
3477
|
+
try {
|
|
3478
|
+
const lower = packageName.toLowerCase();
|
|
3479
|
+
const res = await fetchWithTimeout3(
|
|
3480
|
+
`${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
|
|
3481
|
+
token
|
|
3482
|
+
);
|
|
3483
|
+
if (!res.ok) return null;
|
|
3484
|
+
const data = await res.json();
|
|
3485
|
+
const versions = data.versions ?? [];
|
|
3486
|
+
return versions[versions.length - 1] ?? null;
|
|
3487
|
+
} catch {
|
|
3488
|
+
return null;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
async function getLatestMaven(baseUrl, token, repoName, packageName) {
|
|
3492
|
+
const [groupId, artifactId] = packageName.split(":");
|
|
3493
|
+
if (!groupId || !artifactId) return null;
|
|
3494
|
+
try {
|
|
3495
|
+
const res = await fetchWithTimeout3(
|
|
3496
|
+
`${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
|
|
3497
|
+
token
|
|
3498
|
+
);
|
|
3499
|
+
if (!res.ok) return null;
|
|
3500
|
+
const text = await res.text();
|
|
3501
|
+
return text.trim() || null;
|
|
3502
|
+
} catch {
|
|
3503
|
+
return null;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
async function getOutdatedPackages(packages, config, filterType) {
|
|
3507
|
+
const { baseUrl, token } = config;
|
|
3508
|
+
if (!baseUrl || !token) return { outdated: [], uncheckedEcosystems: [] };
|
|
3509
|
+
const outdated = [];
|
|
3510
|
+
const unchecked = /* @__PURE__ */ new Set();
|
|
3511
|
+
for (const pkg2 of packages) {
|
|
3512
|
+
const repoName = repoForEcosystem(config, pkg2.ecosystem);
|
|
3513
|
+
if (!repoName) {
|
|
3514
|
+
unchecked.add(pkg2.ecosystem);
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
let latest = null;
|
|
3518
|
+
if (pkg2.ecosystem === "npm") {
|
|
3519
|
+
latest = await getLatestNpm(baseUrl, token, repoName, pkg2.name);
|
|
3520
|
+
} else if (pkg2.ecosystem === "nuget") {
|
|
3521
|
+
latest = await getLatestNuget(baseUrl, token, repoName, pkg2.name);
|
|
3522
|
+
} else if (pkg2.ecosystem === "maven") {
|
|
3523
|
+
latest = await getLatestMaven(baseUrl, token, repoName, pkg2.name);
|
|
3524
|
+
}
|
|
3525
|
+
if (!latest || !isNewer(pkg2.version, latest)) continue;
|
|
3526
|
+
const type = updateType(pkg2.version, latest);
|
|
3527
|
+
if (filterType) {
|
|
3528
|
+
const order = { major: 3, minor: 2, patch: 1 };
|
|
3529
|
+
if (order[type] < order[filterType]) continue;
|
|
3530
|
+
}
|
|
3531
|
+
outdated.push({
|
|
3532
|
+
name: pkg2.name,
|
|
3533
|
+
ecosystem: pkg2.ecosystem,
|
|
3534
|
+
current: pkg2.version,
|
|
3535
|
+
latest,
|
|
3536
|
+
updateType: type,
|
|
3537
|
+
source: pkg2.source
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
return { outdated, uncheckedEcosystems: [...unchecked] };
|
|
3541
|
+
}
|
|
3542
|
+
async function getLicenseNpm(baseUrl, token, repoName, packageName) {
|
|
3543
|
+
try {
|
|
3544
|
+
const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
|
|
3545
|
+
const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
|
|
3546
|
+
if (!res.ok) return null;
|
|
3547
|
+
const data = await res.json();
|
|
3548
|
+
if (typeof data.license === "string") return data.license;
|
|
3549
|
+
if (typeof data.license === "object" && data.license !== null) return data.license.type ?? null;
|
|
3550
|
+
return null;
|
|
3551
|
+
} catch {
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
|
|
3556
|
+
try {
|
|
3557
|
+
const lower = packageName.toLowerCase();
|
|
3558
|
+
const res = await fetchWithTimeout3(
|
|
3559
|
+
`${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
|
|
3560
|
+
token
|
|
3561
|
+
);
|
|
3562
|
+
if (!res.ok) return null;
|
|
3563
|
+
const data = await res.json();
|
|
3564
|
+
return data.licenseExpression ?? (data.licenseUrl ? "See licenseUrl" : null);
|
|
3565
|
+
} catch {
|
|
3566
|
+
return null;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
async function getLicensedPackages(packages, config) {
|
|
3570
|
+
const { baseUrl, token } = config;
|
|
3571
|
+
if (!baseUrl || !token) return [];
|
|
3572
|
+
const result = [];
|
|
3573
|
+
for (const pkg2 of packages) {
|
|
3574
|
+
const repoName = repoForEcosystem(config, pkg2.ecosystem);
|
|
3575
|
+
let license = null;
|
|
3576
|
+
if (repoName) {
|
|
3577
|
+
if (pkg2.ecosystem === "npm") {
|
|
3578
|
+
license = await getLicenseNpm(baseUrl, token, repoName, pkg2.name);
|
|
3579
|
+
} else if (pkg2.ecosystem === "nuget") {
|
|
3580
|
+
license = await getLicenseNuget(baseUrl, token, repoName, pkg2.name, pkg2.version);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
result.push({
|
|
3584
|
+
name: pkg2.name,
|
|
3585
|
+
version: pkg2.version,
|
|
3586
|
+
ecosystem: pkg2.ecosystem,
|
|
3587
|
+
source: pkg2.source,
|
|
3588
|
+
license
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
return result;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/services/deps/connectivity.ts
|
|
3595
|
+
var cachedTier = null;
|
|
3596
|
+
async function detectTier(config) {
|
|
3597
|
+
if (cachedTier) return cachedTier;
|
|
3598
|
+
const [osvResult, artResult] = await Promise.all([
|
|
3599
|
+
checkOsvConnectivity(),
|
|
3600
|
+
checkArtifactoryConnectivity(config.artifactory)
|
|
3601
|
+
]);
|
|
3602
|
+
const artifactoryReachable = artResult.reachable && artResult.authenticated;
|
|
3603
|
+
const osvReachable = osvResult.reachable;
|
|
3604
|
+
let tier = "local";
|
|
3605
|
+
if (artifactoryReachable) tier = "artifactory";
|
|
3606
|
+
if (osvReachable) tier = "full";
|
|
3607
|
+
cachedTier = { tier, osvReachable, artifactoryReachable };
|
|
3608
|
+
return cachedTier;
|
|
3609
|
+
}
|
|
3610
|
+
async function buildConnectivityData(config) {
|
|
3611
|
+
const [osvResult, artResult] = await Promise.all([
|
|
3612
|
+
checkOsvConnectivity(),
|
|
3613
|
+
checkArtifactoryConnectivity(config.artifactory)
|
|
3614
|
+
]);
|
|
3615
|
+
const artCfg = config.artifactory;
|
|
3616
|
+
let tier = "local";
|
|
3617
|
+
if (artResult.reachable && artResult.authenticated) tier = "artifactory";
|
|
3618
|
+
if (osvResult.reachable) tier = "full";
|
|
3619
|
+
cachedTier = {
|
|
3620
|
+
tier,
|
|
3621
|
+
osvReachable: osvResult.reachable,
|
|
3622
|
+
artifactoryReachable: artResult.reachable && artResult.authenticated
|
|
3623
|
+
};
|
|
3624
|
+
return {
|
|
3625
|
+
artifactory: {
|
|
3626
|
+
reachable: artResult.reachable,
|
|
3627
|
+
url: artCfg.baseUrl ?? "(not configured)",
|
|
3628
|
+
authenticated: artResult.authenticated,
|
|
3629
|
+
repositories: {
|
|
3630
|
+
npm: artCfg.npmRepo ?? "(not configured)",
|
|
3631
|
+
nuget: artCfg.nugetRepo ?? "(not configured)",
|
|
3632
|
+
maven: artCfg.mavenRepo ?? "(not configured)"
|
|
3633
|
+
},
|
|
3634
|
+
...artResult.error ? { error: artResult.error } : {}
|
|
3635
|
+
},
|
|
3636
|
+
osv: {
|
|
3637
|
+
reachable: osvResult.reachable,
|
|
3638
|
+
url: "https://api.osv.dev",
|
|
3639
|
+
...osvResult.error ? { error: osvResult.error } : {}
|
|
3640
|
+
},
|
|
3641
|
+
tier,
|
|
3642
|
+
capabilities: {
|
|
3643
|
+
scan: true,
|
|
3644
|
+
diff: true,
|
|
3645
|
+
outdated: artResult.reachable && artResult.authenticated,
|
|
3646
|
+
licenseCheck: artResult.reachable && artResult.authenticated,
|
|
3647
|
+
cveCheck: osvResult.reachable
|
|
3648
|
+
}
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// src/services/deps/frisk.ts
|
|
3653
|
+
async function runFrisk(config, opts) {
|
|
3654
|
+
const repoRoot = getRepoRoot2();
|
|
3655
|
+
if (!repoRoot) {
|
|
3656
|
+
throw new PncliError("Not inside a git repository.", 1);
|
|
3657
|
+
}
|
|
3658
|
+
const { tier, osvReachable } = await detectTier(config);
|
|
3659
|
+
if (!osvReachable) {
|
|
3660
|
+
throw new PncliError(
|
|
3661
|
+
"deps frisk requires OSV.dev access but api.osv.dev is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
|
|
3662
|
+
503
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
const scanOpts = {
|
|
3666
|
+
...opts,
|
|
3667
|
+
includeTransitive: opts.includeTransitive ?? true
|
|
3668
|
+
};
|
|
3669
|
+
const scan = scanRepo(repoRoot, scanOpts);
|
|
3670
|
+
if (scan.packages.length === 0) {
|
|
3671
|
+
return { tier, scanned: 0, vulnerable: 0, packages: [] };
|
|
3672
|
+
}
|
|
3673
|
+
const vulnerable = await checkPackagesForVulns(scan.packages);
|
|
3674
|
+
return {
|
|
3675
|
+
tier,
|
|
3676
|
+
scanned: scan.packages.length,
|
|
3677
|
+
vulnerable: vulnerable.length,
|
|
3678
|
+
packages: vulnerable
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
// src/services/deps/outdated.ts
|
|
3683
|
+
async function runOutdated(config, opts, filterType) {
|
|
3684
|
+
const repoRoot = getRepoRoot2();
|
|
3685
|
+
if (!repoRoot) {
|
|
3686
|
+
throw new PncliError("Not inside a git repository.", 1);
|
|
3687
|
+
}
|
|
3688
|
+
requireArtifactory(config.artifactory, "deps outdated");
|
|
3689
|
+
const { artifactoryReachable } = await detectTier(config);
|
|
3690
|
+
if (!artifactoryReachable) {
|
|
3691
|
+
throw new PncliError(
|
|
3692
|
+
`Artifactory at ${config.artifactory.baseUrl} is not reachable or authentication failed. Run 'pncli deps connectivity' to diagnose.`,
|
|
3693
|
+
503
|
|
3694
|
+
);
|
|
3695
|
+
}
|
|
3696
|
+
const scan = scanRepo(repoRoot, { ...opts, includeTransitive: false });
|
|
3697
|
+
const directPackages = scan.packages.filter((p) => p.type === "direct");
|
|
3698
|
+
const { outdated, uncheckedEcosystems } = await getOutdatedPackages(
|
|
3699
|
+
directPackages,
|
|
3700
|
+
config.artifactory,
|
|
3701
|
+
filterType
|
|
3702
|
+
);
|
|
3703
|
+
const summary = { total: outdated.length, major: 0, minor: 0, patch: 0 };
|
|
3704
|
+
for (const pkg2 of outdated) summary[pkg2.updateType]++;
|
|
3705
|
+
return {
|
|
3706
|
+
source: "artifactory",
|
|
3707
|
+
artifactoryUrl: config.artifactory.baseUrl ?? "",
|
|
3708
|
+
outdated,
|
|
3709
|
+
uncheckedEcosystems,
|
|
3710
|
+
summary
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// src/services/deps/license-check.ts
|
|
3715
|
+
async function runLicenseCheck(config, opts) {
|
|
3716
|
+
const repoRoot = getRepoRoot2();
|
|
3717
|
+
if (!repoRoot) {
|
|
3718
|
+
throw new PncliError("Not inside a git repository.", 1);
|
|
3719
|
+
}
|
|
3720
|
+
requireArtifactory(config.artifactory, "deps license-check");
|
|
3721
|
+
const { artifactoryReachable } = await detectTier(config);
|
|
3722
|
+
if (!artifactoryReachable) {
|
|
3723
|
+
throw new PncliError(
|
|
3724
|
+
`Artifactory at ${config.artifactory.baseUrl} is not reachable or authentication failed. Run 'pncli deps connectivity' to diagnose.`,
|
|
3725
|
+
503
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
const scan = scanRepo(repoRoot, { ...opts, includeTransitive: false });
|
|
3729
|
+
const directPackages = scan.packages.filter((p) => p.type === "direct");
|
|
3730
|
+
const licensed = await getLicensedPackages(directPackages, config.artifactory);
|
|
3731
|
+
const byLicense = {};
|
|
3732
|
+
let unknown = 0;
|
|
3733
|
+
for (const pkg2 of licensed) {
|
|
3734
|
+
if (!pkg2.license) {
|
|
3735
|
+
unknown++;
|
|
3736
|
+
} else {
|
|
3737
|
+
byLicense[pkg2.license] = (byLicense[pkg2.license] ?? 0) + 1;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
return {
|
|
3741
|
+
source: "artifactory",
|
|
3742
|
+
artifactoryUrl: config.artifactory.baseUrl ?? "",
|
|
3743
|
+
packages: licensed,
|
|
3744
|
+
summary: {
|
|
3745
|
+
total: licensed.length,
|
|
3746
|
+
byLicense,
|
|
3747
|
+
unknown
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// src/services/deps/commands.ts
|
|
3753
|
+
function registerDepsCommands(program2) {
|
|
3754
|
+
const deps = program2.command("deps").description("Dependency scanning, CVE detection, and license auditing");
|
|
3755
|
+
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) => {
|
|
3756
|
+
const startTime = Date.now();
|
|
3757
|
+
try {
|
|
3758
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3759
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3760
|
+
const scanOpts = {
|
|
3761
|
+
ecosystem: opts.ecosystem,
|
|
3762
|
+
includeTransitive: !opts.directOnly,
|
|
3763
|
+
includeDev: opts.includeDev
|
|
3764
|
+
};
|
|
3765
|
+
const data = await runFrisk(config, scanOpts);
|
|
3766
|
+
success(data, "deps", "frisk", startTime);
|
|
3767
|
+
} catch (err) {
|
|
3768
|
+
fail(err, "deps", "frisk", startTime);
|
|
3769
|
+
}
|
|
3770
|
+
});
|
|
3771
|
+
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) => {
|
|
3772
|
+
const startTime = Date.now();
|
|
3773
|
+
try {
|
|
3774
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3775
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3776
|
+
const scanOpts = {
|
|
3777
|
+
ecosystem: opts.ecosystem,
|
|
3778
|
+
includeTransitive: opts.includeTransitive,
|
|
3779
|
+
includeDev: opts.includeDev
|
|
3780
|
+
};
|
|
3781
|
+
const data = runScan(config, scanOpts);
|
|
3782
|
+
success(data, "deps", "scan", startTime);
|
|
3783
|
+
} catch (err) {
|
|
3784
|
+
fail(err, "deps", "scan", startTime);
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
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) => {
|
|
3788
|
+
const startTime = Date.now();
|
|
3789
|
+
try {
|
|
3790
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3791
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3792
|
+
const scanOpts = {
|
|
3793
|
+
ecosystem: opts.ecosystem,
|
|
3794
|
+
includeTransitive: true,
|
|
3795
|
+
includeDev: opts.includeDev
|
|
3796
|
+
};
|
|
3797
|
+
const data = runDiff(config, opts.from, opts.to ?? null, scanOpts);
|
|
3798
|
+
success(data, "deps", "diff", startTime);
|
|
3799
|
+
} catch (err) {
|
|
3800
|
+
fail(err, "deps", "diff", startTime);
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
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) => {
|
|
3804
|
+
const startTime = Date.now();
|
|
3805
|
+
try {
|
|
3806
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3807
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3808
|
+
const scanOpts = { ecosystem: opts.ecosystem };
|
|
3809
|
+
const filterType = opts.major ? "major" : opts.minor ? "minor" : opts.patch ? "patch" : void 0;
|
|
3810
|
+
const data = await runOutdated(config, scanOpts, filterType);
|
|
3811
|
+
success(data, "deps", "outdated", startTime);
|
|
3812
|
+
} catch (err) {
|
|
3813
|
+
fail(err, "deps", "outdated", startTime);
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
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) => {
|
|
3817
|
+
const startTime = Date.now();
|
|
3818
|
+
try {
|
|
3819
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3820
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3821
|
+
const scanOpts = {
|
|
3822
|
+
ecosystem: opts.ecosystem,
|
|
3823
|
+
includeDev: opts.includeDev
|
|
3824
|
+
};
|
|
3825
|
+
const data = await runLicenseCheck(config, scanOpts);
|
|
3826
|
+
success(data, "deps", "license-check", startTime);
|
|
3827
|
+
} catch (err) {
|
|
3828
|
+
fail(err, "deps", "license-check", startTime);
|
|
3829
|
+
}
|
|
3830
|
+
});
|
|
3831
|
+
deps.command("connectivity").description("Test network access to Artifactory and OSV.dev, report available tier").action(async (_opts, cmd) => {
|
|
3832
|
+
const startTime = Date.now();
|
|
3833
|
+
try {
|
|
3834
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3835
|
+
const config = loadConfig({ configPath: globalOpts.config });
|
|
3836
|
+
const data = await buildConnectivityData(config);
|
|
3837
|
+
success(data, "deps", "connectivity", startTime);
|
|
3838
|
+
} catch (err) {
|
|
3839
|
+
fail(err, "deps", "connectivity", startTime);
|
|
3840
|
+
}
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// src/services/config/commands.ts
|
|
3845
|
+
import { input, password, confirm } from "@inquirer/prompts";
|
|
3846
|
+
import fs5 from "fs";
|
|
3847
|
+
function registerConfigCommands(program2) {
|
|
3848
|
+
const config = program2.command("config").description("Manage pncli configuration");
|
|
3849
|
+
config.command("init").description("Interactive setup wizard").option("--repo", "Write repo config (.pncli.json) instead of global config").action(async (opts) => {
|
|
3850
|
+
const start = Date.now();
|
|
3851
|
+
try {
|
|
3852
|
+
if (opts.repo) {
|
|
3853
|
+
await initRepoConfig(start);
|
|
3854
|
+
} else {
|
|
3855
|
+
await initGlobalConfig(start);
|
|
3856
|
+
}
|
|
3857
|
+
} catch (err) {
|
|
3858
|
+
if (err instanceof Error && err.message.includes("User force closed")) {
|
|
3859
|
+
process.stderr.write("\nSetup cancelled.\n");
|
|
3860
|
+
process.exitCode = ExitCode.GENERAL_ERROR;
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
fail(err, "config", "init", start);
|
|
3864
|
+
}
|
|
3865
|
+
});
|
|
3866
|
+
config.command("show").description("Print fully resolved config (PATs masked)").action(() => {
|
|
3867
|
+
const start = Date.now();
|
|
3868
|
+
try {
|
|
3869
|
+
const resolved = loadConfig();
|
|
3870
|
+
success(maskConfig(resolved), "config", "show", start);
|
|
3871
|
+
} catch (err) {
|
|
3872
|
+
fail(err, "config", "show", start);
|
|
3873
|
+
}
|
|
3874
|
+
});
|
|
3875
|
+
config.command("set").description("Set a config value by dot-notation key (e.g. jira.baseUrl https://...)").argument("<key>", "Config key in dot notation").argument("<value>", "Value to set").action((key, value) => {
|
|
3876
|
+
const start = Date.now();
|
|
3877
|
+
try {
|
|
3878
|
+
const opts = program2.optsWithGlobals();
|
|
3879
|
+
setConfigValue(key, value, opts.config);
|
|
3880
|
+
success({ key, value }, "config", "set", start);
|
|
3881
|
+
} catch (err) {
|
|
3882
|
+
fail(err, "config", "set", start);
|
|
3883
|
+
}
|
|
3884
|
+
});
|
|
3885
|
+
config.command("test").description("Test connectivity to configured services").action(async () => {
|
|
3886
|
+
const start = Date.now();
|
|
3887
|
+
try {
|
|
3888
|
+
const opts = program2.optsWithGlobals();
|
|
3889
|
+
const cfg = loadConfig({ configPath: opts.config });
|
|
3890
|
+
const http = createHttpClient(cfg);
|
|
3891
|
+
const results = {};
|
|
3892
|
+
if (cfg.jira.baseUrl) {
|
|
3893
|
+
try {
|
|
3894
|
+
await http.jira("/rest/api/2/myself");
|
|
3895
|
+
results.jira = { ok: true, message: "connected" };
|
|
3896
|
+
} catch (err) {
|
|
3897
|
+
results.jira = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3898
|
+
}
|
|
3899
|
+
} else {
|
|
3900
|
+
results.jira = { ok: null, message: "not configured" };
|
|
3901
|
+
}
|
|
3902
|
+
if (cfg.bitbucket.baseUrl) {
|
|
3903
|
+
try {
|
|
3904
|
+
await http.bitbucket("/rest/api/1.0/application-properties");
|
|
3905
|
+
results.bitbucket = { ok: true, message: "connected" };
|
|
3906
|
+
} catch (err) {
|
|
3907
|
+
results.bitbucket = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3908
|
+
}
|
|
3909
|
+
} else {
|
|
3910
|
+
results.bitbucket = { ok: null, message: "not configured" };
|
|
3911
|
+
}
|
|
3912
|
+
if (cfg.confluence.baseUrl) {
|
|
3913
|
+
try {
|
|
3914
|
+
await http.confluence("/rest/api/space", { params: { limit: 1 } });
|
|
3915
|
+
results.confluence = { ok: true, message: "connected" };
|
|
3916
|
+
} catch (err) {
|
|
3917
|
+
results.confluence = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3918
|
+
}
|
|
3919
|
+
} else {
|
|
3920
|
+
results.confluence = { ok: null, message: "not configured" };
|
|
3921
|
+
}
|
|
3922
|
+
if (cfg.sonar.baseUrl) {
|
|
3923
|
+
try {
|
|
3924
|
+
await http.sonar("/api/system/status");
|
|
3925
|
+
results.sonar = { ok: true, message: "connected" };
|
|
3926
|
+
} catch (err) {
|
|
3927
|
+
results.sonar = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3928
|
+
}
|
|
3929
|
+
} else {
|
|
3930
|
+
results.sonar = { ok: null, message: "not configured" };
|
|
3931
|
+
}
|
|
3932
|
+
if (cfg.sde.baseUrl) {
|
|
3933
|
+
try {
|
|
3934
|
+
await http.sde("/api/v2/users/me/");
|
|
3935
|
+
results.sde = { ok: true, message: "connected" };
|
|
3936
|
+
} catch (err) {
|
|
3937
|
+
results.sde = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3938
|
+
}
|
|
3939
|
+
} else {
|
|
3940
|
+
results.sde = { ok: null, message: "not configured" };
|
|
3941
|
+
}
|
|
3942
|
+
success(results, "config", "test", start);
|
|
3943
|
+
} catch (err) {
|
|
3944
|
+
fail(err, "config", "test", start);
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
async function initGlobalConfig(start) {
|
|
3949
|
+
process.stderr.write("pncli config init \u2014 Global configuration\n\n");
|
|
3950
|
+
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");
|
|
3951
|
+
const userEmail = await input({
|
|
3952
|
+
message: "Your email address (used across Jira, Bitbucket, etc.):",
|
|
3953
|
+
default: ""
|
|
3954
|
+
});
|
|
3955
|
+
const userId = await input({
|
|
3956
|
+
message: "Your username / user ID:",
|
|
3957
|
+
default: ""
|
|
3958
|
+
});
|
|
3959
|
+
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");
|
|
3960
|
+
const jiraBaseUrl = await input({
|
|
3961
|
+
message: "Jira base URL (e.g. https://jira.your-company.com):",
|
|
3962
|
+
default: ""
|
|
3963
|
+
});
|
|
3964
|
+
const jiraApiToken = await password({
|
|
3965
|
+
message: "Jira personal access token:"
|
|
3966
|
+
});
|
|
3967
|
+
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");
|
|
3968
|
+
const bitbucketBaseUrl = await input({
|
|
3969
|
+
message: "Bitbucket Server base URL (e.g. https://bitbucket.your-company.com):",
|
|
3970
|
+
default: ""
|
|
1524
3971
|
});
|
|
1525
3972
|
const bitbucketPat = await password({
|
|
1526
3973
|
message: "Bitbucket personal access token:"
|
|
1527
3974
|
});
|
|
3975
|
+
process.stderr.write("\n\u2500\u2500 Confluence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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");
|
|
3976
|
+
const confluenceBaseUrl = await input({
|
|
3977
|
+
message: "Confluence base URL (e.g. https://confluence.your-company.com):",
|
|
3978
|
+
default: ""
|
|
3979
|
+
});
|
|
3980
|
+
const confluenceApiToken = await password({
|
|
3981
|
+
message: "Confluence personal access token:"
|
|
3982
|
+
});
|
|
3983
|
+
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");
|
|
3984
|
+
const useArtifactory = await confirm({
|
|
3985
|
+
message: "Configure Artifactory for dependency commands (deps outdated, deps license-check)?",
|
|
3986
|
+
default: false
|
|
3987
|
+
});
|
|
3988
|
+
let artifactoryBaseUrl = "";
|
|
3989
|
+
let artifactoryToken = "";
|
|
3990
|
+
let npmRepo = "";
|
|
3991
|
+
let nugetRepo = "";
|
|
3992
|
+
let mavenRepo = "";
|
|
3993
|
+
if (useArtifactory) {
|
|
3994
|
+
artifactoryBaseUrl = await input({
|
|
3995
|
+
message: "Artifactory base URL (e.g. https://artifactory.company.com):",
|
|
3996
|
+
default: ""
|
|
3997
|
+
});
|
|
3998
|
+
artifactoryToken = await password({
|
|
3999
|
+
message: "Artifactory API token:"
|
|
4000
|
+
});
|
|
4001
|
+
process.stderr.write("\nConfigure which ecosystems you use (skip any that don't apply):\n");
|
|
4002
|
+
const useNpm = await confirm({ message: " Use npm packages from Artifactory?", default: true });
|
|
4003
|
+
if (useNpm) {
|
|
4004
|
+
npmRepo = await input({ message: " npm repository name:", default: "npm-remote" });
|
|
4005
|
+
}
|
|
4006
|
+
const useNuget = await confirm({ message: " Use NuGet packages from Artifactory?", default: false });
|
|
4007
|
+
if (useNuget) {
|
|
4008
|
+
nugetRepo = await input({ message: " NuGet repository name:", default: "nuget-remote" });
|
|
4009
|
+
}
|
|
4010
|
+
const useMaven = await confirm({ message: " Use Maven packages from Artifactory?", default: false });
|
|
4011
|
+
if (useMaven) {
|
|
4012
|
+
mavenRepo = await input({ message: " Maven repository name:", default: "libs-release" });
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
process.stderr.write("\n\u2500\u2500 SonarQube \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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");
|
|
4016
|
+
const useSonar = await confirm({
|
|
4017
|
+
message: "Configure SonarQube Server for code quality checks?",
|
|
4018
|
+
default: false
|
|
4019
|
+
});
|
|
4020
|
+
let sonarBaseUrl = "";
|
|
4021
|
+
let sonarToken = "";
|
|
4022
|
+
if (useSonar) {
|
|
4023
|
+
sonarBaseUrl = await input({
|
|
4024
|
+
message: "SonarQube Server base URL (e.g. https://sonar.your-company.com):",
|
|
4025
|
+
default: ""
|
|
4026
|
+
});
|
|
4027
|
+
sonarToken = await password({
|
|
4028
|
+
message: "SonarQube personal access token:"
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
process.stderr.write("\n\u2500\u2500 SDElements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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");
|
|
4032
|
+
const useSde = await confirm({
|
|
4033
|
+
message: "Configure SDElements for threat modeling and countermeasure queries?",
|
|
4034
|
+
default: false
|
|
4035
|
+
});
|
|
4036
|
+
let sdeBaseUrl = "";
|
|
4037
|
+
let sdeToken = "";
|
|
4038
|
+
if (useSde) {
|
|
4039
|
+
sdeBaseUrl = await input({
|
|
4040
|
+
message: "SDElements base URL\n Cloud-hosted: https://your-org.sdelements.com\n On-premise: https://sde.your-company.com\n URL: ",
|
|
4041
|
+
default: ""
|
|
4042
|
+
});
|
|
4043
|
+
sdeToken = await password({
|
|
4044
|
+
message: "SDElements API token:"
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
1528
4047
|
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");
|
|
1529
4048
|
const jiraProject = await input({
|
|
1530
4049
|
message: "Default Jira project key (optional):",
|
|
1531
4050
|
default: ""
|
|
1532
4051
|
});
|
|
4052
|
+
const sonarProject = useSonar ? await input({
|
|
4053
|
+
message: "Default SonarQube project key (optional):",
|
|
4054
|
+
default: ""
|
|
4055
|
+
}) : "";
|
|
4056
|
+
const sdeProject = useSde ? await input({
|
|
4057
|
+
message: "Default SDElements project ID (optional, numeric):",
|
|
4058
|
+
default: ""
|
|
4059
|
+
}) : "";
|
|
1533
4060
|
process.stderr.write("\n");
|
|
1534
4061
|
const confirmed = await confirm({
|
|
1535
4062
|
message: "Write config to ~/.pncli/config.json?",
|
|
@@ -1537,7 +4064,8 @@ async function initGlobalConfig(start) {
|
|
|
1537
4064
|
});
|
|
1538
4065
|
if (!confirmed) {
|
|
1539
4066
|
process.stderr.write("Aborted.\n");
|
|
1540
|
-
process.
|
|
4067
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
4068
|
+
return;
|
|
1541
4069
|
}
|
|
1542
4070
|
writeGlobalConfig({
|
|
1543
4071
|
user: {
|
|
@@ -1552,10 +4080,47 @@ async function initGlobalConfig(start) {
|
|
|
1552
4080
|
baseUrl: bitbucketBaseUrl || void 0,
|
|
1553
4081
|
pat: bitbucketPat || void 0
|
|
1554
4082
|
},
|
|
4083
|
+
...confluenceBaseUrl || confluenceApiToken ? {
|
|
4084
|
+
confluence: {
|
|
4085
|
+
baseUrl: confluenceBaseUrl || void 0,
|
|
4086
|
+
apiToken: confluenceApiToken || void 0
|
|
4087
|
+
}
|
|
4088
|
+
} : {},
|
|
4089
|
+
...useArtifactory ? {
|
|
4090
|
+
artifactory: {
|
|
4091
|
+
baseUrl: artifactoryBaseUrl || void 0,
|
|
4092
|
+
token: artifactoryToken || void 0,
|
|
4093
|
+
npmRepo: npmRepo || void 0,
|
|
4094
|
+
nugetRepo: nugetRepo || void 0,
|
|
4095
|
+
mavenRepo: mavenRepo || void 0
|
|
4096
|
+
}
|
|
4097
|
+
} : {},
|
|
4098
|
+
...useSonar ? {
|
|
4099
|
+
sonar: {
|
|
4100
|
+
baseUrl: sonarBaseUrl || void 0,
|
|
4101
|
+
token: sonarToken || void 0
|
|
4102
|
+
}
|
|
4103
|
+
} : {},
|
|
4104
|
+
...useSde ? {
|
|
4105
|
+
sde: {
|
|
4106
|
+
baseUrl: sdeBaseUrl || void 0,
|
|
4107
|
+
token: sdeToken || void 0
|
|
4108
|
+
}
|
|
4109
|
+
} : {},
|
|
1555
4110
|
defaults: {
|
|
1556
4111
|
jira: {
|
|
1557
4112
|
project: jiraProject || void 0
|
|
1558
|
-
}
|
|
4113
|
+
},
|
|
4114
|
+
...useSonar && sonarProject ? {
|
|
4115
|
+
sonar: {
|
|
4116
|
+
project: sonarProject || void 0
|
|
4117
|
+
}
|
|
4118
|
+
} : {},
|
|
4119
|
+
...useSde && sdeProject ? {
|
|
4120
|
+
sde: {
|
|
4121
|
+
project: sdeProject || void 0
|
|
4122
|
+
}
|
|
4123
|
+
} : {}
|
|
1559
4124
|
}
|
|
1560
4125
|
});
|
|
1561
4126
|
const configPath = getGlobalConfigPath();
|
|
@@ -1586,16 +4151,18 @@ async function initRepoConfig(start) {
|
|
|
1586
4151
|
});
|
|
1587
4152
|
if (!confirmed) {
|
|
1588
4153
|
process.stderr.write("Aborted.\n");
|
|
1589
|
-
process.
|
|
4154
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
4155
|
+
return;
|
|
1590
4156
|
}
|
|
1591
|
-
if (
|
|
4157
|
+
if (fs5.existsSync(".pncli.json")) {
|
|
1592
4158
|
const overwrite = await confirm({
|
|
1593
4159
|
message: ".pncli.json already exists. Overwrite?",
|
|
1594
4160
|
default: false
|
|
1595
4161
|
});
|
|
1596
4162
|
if (!overwrite) {
|
|
1597
4163
|
process.stderr.write("Aborted.\n");
|
|
1598
|
-
process.
|
|
4164
|
+
process.exitCode = ExitCode.SUCCESS;
|
|
4165
|
+
return;
|
|
1599
4166
|
}
|
|
1600
4167
|
}
|
|
1601
4168
|
writeRepoConfig({
|
|
@@ -1614,6 +4181,9 @@ async function initRepoConfig(start) {
|
|
|
1614
4181
|
}
|
|
1615
4182
|
|
|
1616
4183
|
// src/cli.ts
|
|
4184
|
+
if (!process.env.PNCLI_VERIFY_TLS) {
|
|
4185
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
4186
|
+
}
|
|
1617
4187
|
var require2 = createRequire(import.meta.url);
|
|
1618
4188
|
var pkg = require2("../package.json");
|
|
1619
4189
|
var TAGLINE = "One command does what three meetings couldn't.";
|
|
@@ -1636,21 +4206,24 @@ registerJiraCommands(program);
|
|
|
1636
4206
|
registerBitbucketCommands(program);
|
|
1637
4207
|
registerConfluenceCommands(program);
|
|
1638
4208
|
registerSonarCommands(program);
|
|
1639
|
-
|
|
4209
|
+
registerSdeCommands(program);
|
|
4210
|
+
registerDepsCommands(program);
|
|
1640
4211
|
registerConfigCommands(program);
|
|
1641
4212
|
program.addHelpText("after", `
|
|
1642
4213
|
Services:
|
|
1643
4214
|
git Local git operations (status, diff, log, branch)
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
4215
|
+
deps Dependency scanning, CVE detection, license auditing
|
|
4216
|
+
jira Jira Data Cloud
|
|
4217
|
+
bitbucket Bitbucket Server
|
|
4218
|
+
confluence Confluence
|
|
4219
|
+
sonar SonarQube Server (quality gates, issues, metrics, hotspots)
|
|
4220
|
+
sde SDElements (threat modeling, countermeasures, compliance)
|
|
1649
4221
|
config Manage pncli configuration
|
|
1650
4222
|
`);
|
|
1651
4223
|
program.parseAsync(process.argv).catch((err) => {
|
|
4224
|
+
if (process.exitCode !== void 0) return;
|
|
1652
4225
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
1653
4226
|
`);
|
|
1654
|
-
process.
|
|
4227
|
+
process.exitCode = ExitCode.GENERAL_ERROR;
|
|
1655
4228
|
});
|
|
1656
4229
|
//# sourceMappingURL=cli.js.map
|