@pkgseer/cli 0.1.0-alpha.3 → 0.1.2
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/LICENSE +1 -0
- package/README.md +69 -47
- package/dist/cli.js +4220 -222
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-awxns4wd.js → chunk-2wbvwsc9.js} +1 -1
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
version
|
|
4
|
-
} from "./shared/chunk-
|
|
4
|
+
} from "./shared/chunk-2wbvwsc9.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -12,6 +12,233 @@ import { GraphQLClient } from "graphql-request";
|
|
|
12
12
|
// src/generated/graphql.ts
|
|
13
13
|
import { print } from "graphql";
|
|
14
14
|
import gql from "graphql-tag";
|
|
15
|
+
var CliPackageInfoDocument = gql`
|
|
16
|
+
query CliPackageInfo($registry: Registry!, $name: String!) {
|
|
17
|
+
packageSummary(registry: $registry, name: $name) {
|
|
18
|
+
package {
|
|
19
|
+
name
|
|
20
|
+
registry
|
|
21
|
+
description
|
|
22
|
+
latestVersion
|
|
23
|
+
license
|
|
24
|
+
homepage
|
|
25
|
+
repositoryUrl
|
|
26
|
+
}
|
|
27
|
+
security {
|
|
28
|
+
vulnerabilityCount
|
|
29
|
+
}
|
|
30
|
+
quickstart {
|
|
31
|
+
installCommand
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
var CliPackageVulnsDocument = gql`
|
|
37
|
+
query CliPackageVulns($registry: Registry!, $name: String!, $version: String) {
|
|
38
|
+
packageVulnerabilities(registry: $registry, name: $name, version: $version) {
|
|
39
|
+
package {
|
|
40
|
+
name
|
|
41
|
+
version
|
|
42
|
+
}
|
|
43
|
+
security {
|
|
44
|
+
vulnerabilityCount
|
|
45
|
+
vulnerabilities {
|
|
46
|
+
osvId
|
|
47
|
+
summary
|
|
48
|
+
severityScore
|
|
49
|
+
fixedInVersions
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
var CliPackageQualityDocument = gql`
|
|
56
|
+
query CliPackageQuality($registry: Registry!, $name: String!, $version: String) {
|
|
57
|
+
packageQuality(registry: $registry, name: $name, version: $version) {
|
|
58
|
+
package {
|
|
59
|
+
name
|
|
60
|
+
version
|
|
61
|
+
}
|
|
62
|
+
quality {
|
|
63
|
+
overallScore
|
|
64
|
+
grade
|
|
65
|
+
categories {
|
|
66
|
+
category
|
|
67
|
+
score
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
var CliPackageDepsDocument = gql`
|
|
74
|
+
query CliPackageDeps($registry: Registry!, $name: String!, $version: String, $includeTransitive: Boolean) {
|
|
75
|
+
packageDependencies(
|
|
76
|
+
registry: $registry
|
|
77
|
+
name: $name
|
|
78
|
+
version: $version
|
|
79
|
+
includeTransitive: $includeTransitive
|
|
80
|
+
) {
|
|
81
|
+
package {
|
|
82
|
+
name
|
|
83
|
+
version
|
|
84
|
+
}
|
|
85
|
+
dependencies {
|
|
86
|
+
summary {
|
|
87
|
+
directCount
|
|
88
|
+
uniquePackagesCount
|
|
89
|
+
}
|
|
90
|
+
direct {
|
|
91
|
+
name
|
|
92
|
+
versionConstraint
|
|
93
|
+
type
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
var CliComparePackagesDocument = gql`
|
|
100
|
+
query CliComparePackages($packages: [PackageComparisonInput!]!) {
|
|
101
|
+
comparePackages(packages: $packages) {
|
|
102
|
+
packages {
|
|
103
|
+
packageName
|
|
104
|
+
version
|
|
105
|
+
license
|
|
106
|
+
downloadsLastMonth
|
|
107
|
+
vulnerabilityCount
|
|
108
|
+
quality {
|
|
109
|
+
score
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
var CliDocsSearchDocument = gql`
|
|
116
|
+
query CliDocsSearch($registry: Registry!, $packageName: String!, $keywords: [String!], $query: String, $matchMode: MatchMode, $limit: Int, $version: String, $contextLinesBefore: Int, $contextLinesAfter: Int, $maxMatches: Int) {
|
|
117
|
+
searchPackageDocs(
|
|
118
|
+
registry: $registry
|
|
119
|
+
packageName: $packageName
|
|
120
|
+
keywords: $keywords
|
|
121
|
+
query: $query
|
|
122
|
+
matchMode: $matchMode
|
|
123
|
+
limit: $limit
|
|
124
|
+
version: $version
|
|
125
|
+
) {
|
|
126
|
+
registry
|
|
127
|
+
packageName
|
|
128
|
+
version
|
|
129
|
+
entries {
|
|
130
|
+
slug
|
|
131
|
+
title
|
|
132
|
+
matchCount
|
|
133
|
+
matchedKeywords
|
|
134
|
+
matches(
|
|
135
|
+
contextLinesBefore: $contextLinesBefore
|
|
136
|
+
contextLinesAfter: $contextLinesAfter
|
|
137
|
+
maxMatches: $maxMatches
|
|
138
|
+
) {
|
|
139
|
+
context {
|
|
140
|
+
before
|
|
141
|
+
matchedLines {
|
|
142
|
+
content
|
|
143
|
+
highlights {
|
|
144
|
+
term
|
|
145
|
+
start
|
|
146
|
+
end
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
after
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
var CliProjectDocsSearchDocument = gql`
|
|
157
|
+
query CliProjectDocsSearch($project: String!, $keywords: [String!], $query: String, $matchMode: MatchMode, $limit: Int, $contextLinesBefore: Int, $contextLinesAfter: Int, $maxMatches: Int) {
|
|
158
|
+
searchProjectDocs(
|
|
159
|
+
project: $project
|
|
160
|
+
keywords: $keywords
|
|
161
|
+
query: $query
|
|
162
|
+
matchMode: $matchMode
|
|
163
|
+
limit: $limit
|
|
164
|
+
) {
|
|
165
|
+
entries {
|
|
166
|
+
slug
|
|
167
|
+
title
|
|
168
|
+
packageName
|
|
169
|
+
registry
|
|
170
|
+
version
|
|
171
|
+
matchCount
|
|
172
|
+
matchedKeywords
|
|
173
|
+
matches(
|
|
174
|
+
contextLinesBefore: $contextLinesBefore
|
|
175
|
+
contextLinesAfter: $contextLinesAfter
|
|
176
|
+
maxMatches: $maxMatches
|
|
177
|
+
) {
|
|
178
|
+
context {
|
|
179
|
+
before
|
|
180
|
+
matchedLines {
|
|
181
|
+
content
|
|
182
|
+
highlights {
|
|
183
|
+
term
|
|
184
|
+
start
|
|
185
|
+
end
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
after
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
195
|
+
var CliDocsListDocument = gql`
|
|
196
|
+
query CliDocsList($registry: Registry!, $packageName: String!, $version: String) {
|
|
197
|
+
listPackageDocs(
|
|
198
|
+
registry: $registry
|
|
199
|
+
packageName: $packageName
|
|
200
|
+
version: $version
|
|
201
|
+
) {
|
|
202
|
+
packageName
|
|
203
|
+
version
|
|
204
|
+
pages {
|
|
205
|
+
slug
|
|
206
|
+
title
|
|
207
|
+
words
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
var CliDocsGetDocument = gql`
|
|
213
|
+
query CliDocsGet($registry: Registry!, $packageName: String!, $pageId: String!, $version: String) {
|
|
214
|
+
fetchPackageDoc(
|
|
215
|
+
registry: $registry
|
|
216
|
+
packageName: $packageName
|
|
217
|
+
pageId: $pageId
|
|
218
|
+
version: $version
|
|
219
|
+
) {
|
|
220
|
+
page {
|
|
221
|
+
title
|
|
222
|
+
content
|
|
223
|
+
breadcrumbs
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
var CreateProjectDocument = gql`
|
|
229
|
+
mutation CreateProject($input: CreateProjectInput!) {
|
|
230
|
+
createProject(input: $input) {
|
|
231
|
+
project {
|
|
232
|
+
name
|
|
233
|
+
defaultBranch
|
|
234
|
+
}
|
|
235
|
+
errors {
|
|
236
|
+
field
|
|
237
|
+
message
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
`;
|
|
15
242
|
var PackageSummaryDocument = gql`
|
|
16
243
|
query PackageSummary($registry: Registry!, $name: String!) {
|
|
17
244
|
packageSummary(registry: $registry, name: $name) {
|
|
@@ -182,14 +409,179 @@ var ComparePackagesDocument = gql`
|
|
|
182
409
|
}
|
|
183
410
|
}
|
|
184
411
|
`;
|
|
412
|
+
var ListPackageDocsDocument = gql`
|
|
413
|
+
query ListPackageDocs($registry: Registry!, $packageName: String!, $version: String) {
|
|
414
|
+
listPackageDocs(
|
|
415
|
+
registry: $registry
|
|
416
|
+
packageName: $packageName
|
|
417
|
+
version: $version
|
|
418
|
+
) {
|
|
419
|
+
schemaVersion
|
|
420
|
+
registry
|
|
421
|
+
packageName
|
|
422
|
+
version
|
|
423
|
+
stale
|
|
424
|
+
pages {
|
|
425
|
+
id
|
|
426
|
+
title
|
|
427
|
+
slug
|
|
428
|
+
order
|
|
429
|
+
linkName
|
|
430
|
+
words
|
|
431
|
+
lastUpdatedAt
|
|
432
|
+
sourceUrl
|
|
433
|
+
}
|
|
434
|
+
metadata
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`;
|
|
438
|
+
var FetchPackageDocDocument = gql`
|
|
439
|
+
query FetchPackageDoc($registry: Registry!, $packageName: String!, $pageId: String!, $version: String) {
|
|
440
|
+
fetchPackageDoc(
|
|
441
|
+
registry: $registry
|
|
442
|
+
packageName: $packageName
|
|
443
|
+
pageId: $pageId
|
|
444
|
+
version: $version
|
|
445
|
+
) {
|
|
446
|
+
schemaVersion
|
|
447
|
+
registry
|
|
448
|
+
packageName
|
|
449
|
+
version
|
|
450
|
+
page {
|
|
451
|
+
id
|
|
452
|
+
title
|
|
453
|
+
content
|
|
454
|
+
contentFormat
|
|
455
|
+
breadcrumbs
|
|
456
|
+
linkName
|
|
457
|
+
linkTargets
|
|
458
|
+
lastUpdatedAt
|
|
459
|
+
source {
|
|
460
|
+
url
|
|
461
|
+
label
|
|
462
|
+
}
|
|
463
|
+
baseUrl
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
`;
|
|
468
|
+
var SearchPackageDocsDocument = gql`
|
|
469
|
+
query SearchPackageDocs($registry: Registry!, $packageName: String!, $keywords: [String!], $query: String, $includeSnippets: Boolean, $limit: Int, $version: String) {
|
|
470
|
+
searchPackageDocs(
|
|
471
|
+
registry: $registry
|
|
472
|
+
packageName: $packageName
|
|
473
|
+
keywords: $keywords
|
|
474
|
+
query: $query
|
|
475
|
+
includeSnippets: $includeSnippets
|
|
476
|
+
limit: $limit
|
|
477
|
+
version: $version
|
|
478
|
+
) {
|
|
479
|
+
schemaVersion
|
|
480
|
+
registry
|
|
481
|
+
packageName
|
|
482
|
+
version
|
|
483
|
+
query
|
|
484
|
+
entries {
|
|
485
|
+
id
|
|
486
|
+
title
|
|
487
|
+
slug
|
|
488
|
+
order
|
|
489
|
+
linkName
|
|
490
|
+
words
|
|
491
|
+
lastUpdatedAt
|
|
492
|
+
sourceUrl
|
|
493
|
+
matchCount
|
|
494
|
+
titleHit
|
|
495
|
+
score
|
|
496
|
+
matchedKeywords
|
|
497
|
+
snippet
|
|
498
|
+
}
|
|
499
|
+
metadata
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
`;
|
|
503
|
+
var SearchProjectDocsDocument = gql`
|
|
504
|
+
query SearchProjectDocs($project: String!, $keywords: [String!], $query: String, $includeSnippets: Boolean, $limit: Int) {
|
|
505
|
+
searchProjectDocs(
|
|
506
|
+
project: $project
|
|
507
|
+
keywords: $keywords
|
|
508
|
+
query: $query
|
|
509
|
+
includeSnippets: $includeSnippets
|
|
510
|
+
limit: $limit
|
|
511
|
+
) {
|
|
512
|
+
schemaVersion
|
|
513
|
+
project
|
|
514
|
+
query
|
|
515
|
+
entries {
|
|
516
|
+
id
|
|
517
|
+
title
|
|
518
|
+
slug
|
|
519
|
+
registry
|
|
520
|
+
packageName
|
|
521
|
+
version
|
|
522
|
+
words
|
|
523
|
+
matchCount
|
|
524
|
+
titleHit
|
|
525
|
+
score
|
|
526
|
+
matchedKeywords
|
|
527
|
+
snippet
|
|
528
|
+
}
|
|
529
|
+
metadata
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
185
533
|
var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
|
|
534
|
+
var CliPackageInfoDocumentString = print(CliPackageInfoDocument);
|
|
535
|
+
var CliPackageVulnsDocumentString = print(CliPackageVulnsDocument);
|
|
536
|
+
var CliPackageQualityDocumentString = print(CliPackageQualityDocument);
|
|
537
|
+
var CliPackageDepsDocumentString = print(CliPackageDepsDocument);
|
|
538
|
+
var CliComparePackagesDocumentString = print(CliComparePackagesDocument);
|
|
539
|
+
var CliDocsSearchDocumentString = print(CliDocsSearchDocument);
|
|
540
|
+
var CliProjectDocsSearchDocumentString = print(CliProjectDocsSearchDocument);
|
|
541
|
+
var CliDocsListDocumentString = print(CliDocsListDocument);
|
|
542
|
+
var CliDocsGetDocumentString = print(CliDocsGetDocument);
|
|
543
|
+
var CreateProjectDocumentString = print(CreateProjectDocument);
|
|
186
544
|
var PackageSummaryDocumentString = print(PackageSummaryDocument);
|
|
187
545
|
var PackageVulnerabilitiesDocumentString = print(PackageVulnerabilitiesDocument);
|
|
188
546
|
var PackageDependenciesDocumentString = print(PackageDependenciesDocument);
|
|
189
547
|
var PackageQualityDocumentString = print(PackageQualityDocument);
|
|
190
548
|
var ComparePackagesDocumentString = print(ComparePackagesDocument);
|
|
549
|
+
var ListPackageDocsDocumentString = print(ListPackageDocsDocument);
|
|
550
|
+
var FetchPackageDocDocumentString = print(FetchPackageDocDocument);
|
|
551
|
+
var SearchPackageDocsDocumentString = print(SearchPackageDocsDocument);
|
|
552
|
+
var SearchProjectDocsDocumentString = print(SearchProjectDocsDocument);
|
|
191
553
|
function getSdk(client, withWrapper = defaultWrapper) {
|
|
192
554
|
return {
|
|
555
|
+
CliPackageInfo(variables, requestHeaders) {
|
|
556
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliPackageInfoDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliPackageInfo", "query", variables);
|
|
557
|
+
},
|
|
558
|
+
CliPackageVulns(variables, requestHeaders) {
|
|
559
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliPackageVulnsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliPackageVulns", "query", variables);
|
|
560
|
+
},
|
|
561
|
+
CliPackageQuality(variables, requestHeaders) {
|
|
562
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliPackageQualityDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliPackageQuality", "query", variables);
|
|
563
|
+
},
|
|
564
|
+
CliPackageDeps(variables, requestHeaders) {
|
|
565
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliPackageDepsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliPackageDeps", "query", variables);
|
|
566
|
+
},
|
|
567
|
+
CliComparePackages(variables, requestHeaders) {
|
|
568
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliComparePackagesDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliComparePackages", "query", variables);
|
|
569
|
+
},
|
|
570
|
+
CliDocsSearch(variables, requestHeaders) {
|
|
571
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliDocsSearchDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliDocsSearch", "query", variables);
|
|
572
|
+
},
|
|
573
|
+
CliProjectDocsSearch(variables, requestHeaders) {
|
|
574
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliProjectDocsSearchDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliProjectDocsSearch", "query", variables);
|
|
575
|
+
},
|
|
576
|
+
CliDocsList(variables, requestHeaders) {
|
|
577
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliDocsListDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliDocsList", "query", variables);
|
|
578
|
+
},
|
|
579
|
+
CliDocsGet(variables, requestHeaders) {
|
|
580
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CliDocsGetDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CliDocsGet", "query", variables);
|
|
581
|
+
},
|
|
582
|
+
CreateProject(variables, requestHeaders) {
|
|
583
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(CreateProjectDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "CreateProject", "mutation", variables);
|
|
584
|
+
},
|
|
193
585
|
PackageSummary(variables, requestHeaders) {
|
|
194
586
|
return withWrapper((wrappedRequestHeaders) => client.rawRequest(PackageSummaryDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "PackageSummary", "query", variables);
|
|
195
587
|
},
|
|
@@ -204,6 +596,18 @@ function getSdk(client, withWrapper = defaultWrapper) {
|
|
|
204
596
|
},
|
|
205
597
|
ComparePackages(variables, requestHeaders) {
|
|
206
598
|
return withWrapper((wrappedRequestHeaders) => client.rawRequest(ComparePackagesDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "ComparePackages", "query", variables);
|
|
599
|
+
},
|
|
600
|
+
ListPackageDocs(variables, requestHeaders) {
|
|
601
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(ListPackageDocsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "ListPackageDocs", "query", variables);
|
|
602
|
+
},
|
|
603
|
+
FetchPackageDoc(variables, requestHeaders) {
|
|
604
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(FetchPackageDocDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "FetchPackageDoc", "query", variables);
|
|
605
|
+
},
|
|
606
|
+
SearchPackageDocs(variables, requestHeaders) {
|
|
607
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(SearchPackageDocsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "SearchPackageDocs", "query", variables);
|
|
608
|
+
},
|
|
609
|
+
SearchProjectDocs(variables, requestHeaders) {
|
|
610
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(SearchProjectDocsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "SearchProjectDocs", "query", variables);
|
|
207
611
|
}
|
|
208
612
|
};
|
|
209
613
|
}
|
|
@@ -420,7 +824,7 @@ class AuthStorageImpl {
|
|
|
420
824
|
const normalizedUrl = normalizeBaseUrl(baseUrl);
|
|
421
825
|
stored.tokens[normalizedUrl] = token;
|
|
422
826
|
stored.version = CURRENT_VERSION;
|
|
423
|
-
await this.fs.ensureDir(this.
|
|
827
|
+
await this.fs.ensureDir(this.configDir, DIR_MODE);
|
|
424
828
|
await this.fs.writeFile(this.authPath, JSON.stringify(stored, null, 2), FILE_MODE);
|
|
425
829
|
}
|
|
426
830
|
async clear(baseUrl) {
|
|
@@ -494,6 +898,21 @@ function migrateV2ToV3(legacy) {
|
|
|
494
898
|
tokens
|
|
495
899
|
};
|
|
496
900
|
}
|
|
901
|
+
// src/services/auth-utils.ts
|
|
902
|
+
var PROJECT_MANIFEST_UPLOAD_SCOPE = "project_manifest_upload";
|
|
903
|
+
async function checkProjectWriteScope(configService, baseUrl) {
|
|
904
|
+
const tokenData = await configService.getApiToken(baseUrl);
|
|
905
|
+
if (!tokenData) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
if (tokenData.scopes.length === 0) {
|
|
909
|
+
return tokenData;
|
|
910
|
+
}
|
|
911
|
+
if (!tokenData.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
return tokenData;
|
|
915
|
+
}
|
|
497
916
|
// src/services/browser-service.ts
|
|
498
917
|
import open from "open";
|
|
499
918
|
|
|
@@ -502,8 +921,167 @@ class BrowserServiceImpl {
|
|
|
502
921
|
await open(url);
|
|
503
922
|
}
|
|
504
923
|
}
|
|
924
|
+
// src/services/config-service.ts
|
|
925
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
926
|
+
import { z } from "zod";
|
|
927
|
+
var CONFIG_DIR2 = ".pkgseer";
|
|
928
|
+
var GLOBAL_CONFIG_FILE = "config.yml";
|
|
929
|
+
var PROJECT_CONFIG_FILE = "pkgseer.yml";
|
|
930
|
+
var TOOL_NAMES = [
|
|
931
|
+
"package_summary",
|
|
932
|
+
"package_vulnerabilities",
|
|
933
|
+
"package_dependencies",
|
|
934
|
+
"package_quality",
|
|
935
|
+
"compare_packages",
|
|
936
|
+
"list_package_docs",
|
|
937
|
+
"fetch_package_doc",
|
|
938
|
+
"search_package_docs",
|
|
939
|
+
"search_project_docs"
|
|
940
|
+
];
|
|
941
|
+
var ToolNameSchema = z.enum(TOOL_NAMES);
|
|
942
|
+
var SharedConfigFields = {
|
|
943
|
+
enabled_tools: z.array(ToolNameSchema).optional()
|
|
944
|
+
};
|
|
945
|
+
var GlobalConfigSchema = z.object({
|
|
946
|
+
...SharedConfigFields
|
|
947
|
+
});
|
|
948
|
+
var ManifestGroupSchema = z.object({
|
|
949
|
+
label: z.string(),
|
|
950
|
+
files: z.array(z.string()),
|
|
951
|
+
allow_mix_deps: z.boolean().optional()
|
|
952
|
+
});
|
|
953
|
+
var ProjectConfigSchema = z.object({
|
|
954
|
+
...SharedConfigFields,
|
|
955
|
+
project: z.string().optional(),
|
|
956
|
+
manifests: z.array(ManifestGroupSchema).optional()
|
|
957
|
+
});
|
|
958
|
+
var MergedConfigSchema = z.object({
|
|
959
|
+
enabled_tools: z.array(ToolNameSchema).optional(),
|
|
960
|
+
project: z.string().optional(),
|
|
961
|
+
manifests: z.array(ManifestGroupSchema).optional()
|
|
962
|
+
});
|
|
963
|
+
var PKGSEER_API_TOKEN = "PKGSEER_API_TOKEN";
|
|
964
|
+
|
|
965
|
+
class ConfigServiceImpl {
|
|
966
|
+
fs;
|
|
967
|
+
authStorage;
|
|
968
|
+
constructor(fs, authStorage) {
|
|
969
|
+
this.fs = fs;
|
|
970
|
+
this.authStorage = authStorage;
|
|
971
|
+
}
|
|
972
|
+
getGlobalConfigPath() {
|
|
973
|
+
return this.fs.joinPath(this.fs.getHomeDir(), CONFIG_DIR2, GLOBAL_CONFIG_FILE);
|
|
974
|
+
}
|
|
975
|
+
async loadGlobalConfig() {
|
|
976
|
+
const configPath = this.getGlobalConfigPath();
|
|
977
|
+
return this.loadAndParseConfig(configPath, GlobalConfigSchema);
|
|
978
|
+
}
|
|
979
|
+
async loadProjectConfig() {
|
|
980
|
+
let currentDir = this.fs.getCwd();
|
|
981
|
+
while (true) {
|
|
982
|
+
const configPath = this.fs.joinPath(currentDir, PROJECT_CONFIG_FILE);
|
|
983
|
+
const config = await this.loadAndParseConfig(configPath, ProjectConfigSchema);
|
|
984
|
+
if (config) {
|
|
985
|
+
return { config, path: configPath };
|
|
986
|
+
}
|
|
987
|
+
const parentDir = this.fs.getDirname(currentDir);
|
|
988
|
+
if (parentDir === currentDir) {
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
currentDir = parentDir;
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
async loadMergedConfig() {
|
|
996
|
+
const [globalConfig, projectResult] = await Promise.all([
|
|
997
|
+
this.loadGlobalConfig(),
|
|
998
|
+
this.loadProjectConfig()
|
|
999
|
+
]);
|
|
1000
|
+
const merged = {};
|
|
1001
|
+
if (globalConfig?.enabled_tools) {
|
|
1002
|
+
merged.enabled_tools = globalConfig.enabled_tools;
|
|
1003
|
+
}
|
|
1004
|
+
if (projectResult?.config) {
|
|
1005
|
+
if (projectResult.config.enabled_tools) {
|
|
1006
|
+
merged.enabled_tools = projectResult.config.enabled_tools;
|
|
1007
|
+
}
|
|
1008
|
+
if (projectResult.config.project) {
|
|
1009
|
+
merged.project = projectResult.config.project;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
config: merged,
|
|
1014
|
+
globalPath: globalConfig ? this.getGlobalConfigPath() : null,
|
|
1015
|
+
projectPath: projectResult?.path ?? null
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
async writeProjectConfig(config) {
|
|
1019
|
+
const validated = ProjectConfigSchema.parse(config);
|
|
1020
|
+
const currentDir = this.fs.getCwd();
|
|
1021
|
+
const configPath = this.fs.joinPath(currentDir, PROJECT_CONFIG_FILE);
|
|
1022
|
+
const existing = await this.loadProjectConfig();
|
|
1023
|
+
const existingConfig = existing?.config ?? {};
|
|
1024
|
+
const mergedConfig = {
|
|
1025
|
+
...existingConfig,
|
|
1026
|
+
...validated
|
|
1027
|
+
};
|
|
1028
|
+
const yamlContent = stringifyYaml(mergedConfig, {
|
|
1029
|
+
defaultStringType: "PLAIN",
|
|
1030
|
+
nullStr: ""
|
|
1031
|
+
});
|
|
1032
|
+
await this.fs.writeFile(configPath, yamlContent);
|
|
1033
|
+
}
|
|
1034
|
+
async loadAndParseConfig(path, schema) {
|
|
1035
|
+
const exists = await this.fs.exists(path);
|
|
1036
|
+
if (!exists) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
const content = await this.fs.readFile(path);
|
|
1041
|
+
const parsed = parseYaml(content);
|
|
1042
|
+
if (parsed === null || parsed === undefined) {
|
|
1043
|
+
return schema.parse({});
|
|
1044
|
+
}
|
|
1045
|
+
const result = schema.safeParse(parsed);
|
|
1046
|
+
if (result.success) {
|
|
1047
|
+
return result.data;
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
} catch {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async getApiToken(baseUrl) {
|
|
1055
|
+
const envToken = process.env[PKGSEER_API_TOKEN];
|
|
1056
|
+
if (envToken) {
|
|
1057
|
+
return {
|
|
1058
|
+
token: envToken,
|
|
1059
|
+
tokenName: "PKGSEER_API_TOKEN",
|
|
1060
|
+
scopes: [],
|
|
1061
|
+
createdAt: new Date().toISOString(),
|
|
1062
|
+
expiresAt: null,
|
|
1063
|
+
apiKeyId: 0
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const stored = await this.authStorage.load(baseUrl);
|
|
1067
|
+
if (!stored) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
if (stored.expiresAt && new Date(stored.expiresAt) < new Date) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
return stored;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
505
1076
|
// src/services/filesystem-service.ts
|
|
506
|
-
import {
|
|
1077
|
+
import {
|
|
1078
|
+
mkdir,
|
|
1079
|
+
readdir,
|
|
1080
|
+
readFile,
|
|
1081
|
+
stat,
|
|
1082
|
+
unlink,
|
|
1083
|
+
writeFile
|
|
1084
|
+
} from "node:fs/promises";
|
|
507
1085
|
import { homedir } from "node:os";
|
|
508
1086
|
import { dirname, join } from "node:path";
|
|
509
1087
|
|
|
@@ -532,7 +1110,7 @@ class FileSystemServiceImpl {
|
|
|
532
1110
|
}
|
|
533
1111
|
}
|
|
534
1112
|
async ensureDir(path, mode) {
|
|
535
|
-
await mkdir(
|
|
1113
|
+
await mkdir(path, { recursive: true, mode });
|
|
536
1114
|
}
|
|
537
1115
|
getHomeDir() {
|
|
538
1116
|
return homedir();
|
|
@@ -540,6 +1118,50 @@ class FileSystemServiceImpl {
|
|
|
540
1118
|
joinPath(...segments) {
|
|
541
1119
|
return join(...segments);
|
|
542
1120
|
}
|
|
1121
|
+
getCwd() {
|
|
1122
|
+
return process.cwd();
|
|
1123
|
+
}
|
|
1124
|
+
getDirname(path) {
|
|
1125
|
+
return dirname(path);
|
|
1126
|
+
}
|
|
1127
|
+
async readdir(path) {
|
|
1128
|
+
return readdir(path);
|
|
1129
|
+
}
|
|
1130
|
+
async isDirectory(path) {
|
|
1131
|
+
try {
|
|
1132
|
+
const stats = await stat(path);
|
|
1133
|
+
return stats.isDirectory();
|
|
1134
|
+
} catch {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// src/services/git-service.ts
|
|
1140
|
+
import { exec } from "node:child_process";
|
|
1141
|
+
import { existsSync } from "node:fs";
|
|
1142
|
+
import { join as join2 } from "node:path";
|
|
1143
|
+
import { promisify } from "node:util";
|
|
1144
|
+
var execAsync = promisify(exec);
|
|
1145
|
+
|
|
1146
|
+
class GitServiceImpl {
|
|
1147
|
+
cwd;
|
|
1148
|
+
constructor(cwd = process.cwd()) {
|
|
1149
|
+
this.cwd = cwd;
|
|
1150
|
+
}
|
|
1151
|
+
async getCurrentBranch() {
|
|
1152
|
+
try {
|
|
1153
|
+
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
1154
|
+
cwd: this.cwd
|
|
1155
|
+
});
|
|
1156
|
+
return stdout.trim() || null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async isGitRepository() {
|
|
1162
|
+
const gitPath = join2(this.cwd, ".git");
|
|
1163
|
+
return existsSync(gitPath);
|
|
1164
|
+
}
|
|
543
1165
|
}
|
|
544
1166
|
// src/services/pkgseer-service.ts
|
|
545
1167
|
class PkgseerServiceImpl {
|
|
@@ -581,27 +1203,352 @@ class PkgseerServiceImpl {
|
|
|
581
1203
|
const result = await this.client.ComparePackages({ packages });
|
|
582
1204
|
return { data: result.data, errors: result.errors };
|
|
583
1205
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1206
|
+
async listPackageDocs(registry, packageName, version2) {
|
|
1207
|
+
const result = await this.client.ListPackageDocs({
|
|
1208
|
+
registry,
|
|
1209
|
+
packageName,
|
|
1210
|
+
version: version2
|
|
1211
|
+
});
|
|
1212
|
+
return { data: result.data, errors: result.errors };
|
|
590
1213
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1214
|
+
async fetchPackageDoc(registry, packageName, pageId, version2) {
|
|
1215
|
+
const result = await this.client.FetchPackageDoc({
|
|
1216
|
+
registry,
|
|
1217
|
+
packageName,
|
|
1218
|
+
pageId,
|
|
1219
|
+
version: version2
|
|
1220
|
+
});
|
|
1221
|
+
return { data: result.data, errors: result.errors };
|
|
1222
|
+
}
|
|
1223
|
+
async searchPackageDocs(registry, packageName, options) {
|
|
1224
|
+
const result = await this.client.SearchPackageDocs({
|
|
1225
|
+
registry,
|
|
1226
|
+
packageName,
|
|
1227
|
+
keywords: options?.keywords,
|
|
1228
|
+
query: options?.query,
|
|
1229
|
+
includeSnippets: options?.includeSnippets,
|
|
1230
|
+
limit: options?.limit,
|
|
1231
|
+
version: options?.version
|
|
1232
|
+
});
|
|
1233
|
+
return { data: result.data, errors: result.errors };
|
|
1234
|
+
}
|
|
1235
|
+
async searchProjectDocs(project, options) {
|
|
1236
|
+
const result = await this.client.SearchProjectDocs({
|
|
1237
|
+
project,
|
|
1238
|
+
keywords: options?.keywords,
|
|
1239
|
+
query: options?.query,
|
|
1240
|
+
includeSnippets: options?.includeSnippets,
|
|
1241
|
+
limit: options?.limit
|
|
1242
|
+
});
|
|
1243
|
+
return { data: result.data, errors: result.errors };
|
|
1244
|
+
}
|
|
1245
|
+
async cliPackageInfo(registry, name) {
|
|
1246
|
+
const result = await this.client.CliPackageInfo({ registry, name });
|
|
1247
|
+
return { data: result.data, errors: result.errors };
|
|
1248
|
+
}
|
|
1249
|
+
async cliPackageVulns(registry, name, version2) {
|
|
1250
|
+
const result = await this.client.CliPackageVulns({
|
|
1251
|
+
registry,
|
|
1252
|
+
name,
|
|
1253
|
+
version: version2
|
|
1254
|
+
});
|
|
1255
|
+
return { data: result.data, errors: result.errors };
|
|
1256
|
+
}
|
|
1257
|
+
async cliPackageQuality(registry, name, version2) {
|
|
1258
|
+
const result = await this.client.CliPackageQuality({
|
|
1259
|
+
registry,
|
|
1260
|
+
name,
|
|
1261
|
+
version: version2
|
|
1262
|
+
});
|
|
1263
|
+
return { data: result.data, errors: result.errors };
|
|
1264
|
+
}
|
|
1265
|
+
async cliPackageDeps(registry, name, version2, includeTransitive) {
|
|
1266
|
+
const result = await this.client.CliPackageDeps({
|
|
1267
|
+
registry,
|
|
1268
|
+
name,
|
|
1269
|
+
version: version2,
|
|
1270
|
+
includeTransitive
|
|
1271
|
+
});
|
|
1272
|
+
return { data: result.data, errors: result.errors };
|
|
1273
|
+
}
|
|
1274
|
+
async cliComparePackages(packages) {
|
|
1275
|
+
const result = await this.client.CliComparePackages({ packages });
|
|
1276
|
+
return { data: result.data, errors: result.errors };
|
|
1277
|
+
}
|
|
1278
|
+
async cliDocsList(registry, packageName, version2) {
|
|
1279
|
+
const result = await this.client.CliDocsList({
|
|
1280
|
+
registry,
|
|
1281
|
+
packageName,
|
|
1282
|
+
version: version2
|
|
1283
|
+
});
|
|
1284
|
+
return { data: result.data, errors: result.errors };
|
|
1285
|
+
}
|
|
1286
|
+
async cliDocsGet(registry, packageName, pageId, version2) {
|
|
1287
|
+
const result = await this.client.CliDocsGet({
|
|
1288
|
+
registry,
|
|
1289
|
+
packageName,
|
|
1290
|
+
pageId,
|
|
1291
|
+
version: version2
|
|
1292
|
+
});
|
|
1293
|
+
return { data: result.data, errors: result.errors };
|
|
1294
|
+
}
|
|
1295
|
+
async cliDocsSearch(registry, packageName, options) {
|
|
1296
|
+
const result = await this.client.CliDocsSearch({
|
|
1297
|
+
registry,
|
|
1298
|
+
packageName,
|
|
1299
|
+
keywords: options?.keywords,
|
|
1300
|
+
query: options?.query,
|
|
1301
|
+
matchMode: options?.matchMode,
|
|
1302
|
+
limit: options?.limit,
|
|
1303
|
+
version: options?.version,
|
|
1304
|
+
contextLinesBefore: options?.contextLinesBefore,
|
|
1305
|
+
contextLinesAfter: options?.contextLinesAfter,
|
|
1306
|
+
maxMatches: options?.maxMatches
|
|
1307
|
+
});
|
|
1308
|
+
return { data: result.data, errors: result.errors };
|
|
1309
|
+
}
|
|
1310
|
+
async cliProjectDocsSearch(project, options) {
|
|
1311
|
+
const result = await this.client.CliProjectDocsSearch({
|
|
1312
|
+
project,
|
|
1313
|
+
keywords: options?.keywords,
|
|
1314
|
+
query: options?.query,
|
|
1315
|
+
matchMode: options?.matchMode,
|
|
1316
|
+
limit: options?.limit,
|
|
1317
|
+
contextLinesBefore: options?.contextLinesBefore,
|
|
1318
|
+
contextLinesAfter: options?.contextLinesAfter,
|
|
1319
|
+
maxMatches: options?.maxMatches
|
|
1320
|
+
});
|
|
1321
|
+
return { data: result.data, errors: result.errors };
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
// src/services/project-service.ts
|
|
1325
|
+
var PROJECT_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
1326
|
+
var MIN_NAME_LENGTH = 1;
|
|
1327
|
+
var MAX_NAME_LENGTH = 100;
|
|
1328
|
+
function validateProjectName(name) {
|
|
1329
|
+
const trimmedName = name?.trim() ?? "";
|
|
1330
|
+
if (!trimmedName || trimmedName.length === 0) {
|
|
1331
|
+
return { valid: false, error: "Project name cannot be empty" };
|
|
1332
|
+
}
|
|
1333
|
+
if (trimmedName.length < MIN_NAME_LENGTH) {
|
|
1334
|
+
return {
|
|
1335
|
+
valid: false,
|
|
1336
|
+
error: `Project name must be at least ${MIN_NAME_LENGTH} character`
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
if (trimmedName.length > MAX_NAME_LENGTH) {
|
|
1340
|
+
return {
|
|
1341
|
+
valid: false,
|
|
1342
|
+
error: `Project name must be at most ${MAX_NAME_LENGTH} characters`
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (!PROJECT_NAME_REGEX.test(trimmedName)) {
|
|
1346
|
+
return {
|
|
1347
|
+
valid: false,
|
|
1348
|
+
error: "Project name can only contain alphanumeric characters, hyphens, and underscores, and must start with an alphanumeric character"
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
return { valid: true };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
class ProjectServiceImpl {
|
|
1355
|
+
client;
|
|
1356
|
+
baseUrl;
|
|
1357
|
+
apiToken;
|
|
1358
|
+
constructor(client, baseUrl, apiToken) {
|
|
1359
|
+
this.client = client;
|
|
1360
|
+
this.baseUrl = baseUrl;
|
|
1361
|
+
this.apiToken = apiToken;
|
|
1362
|
+
}
|
|
1363
|
+
async createProject(input) {
|
|
1364
|
+
const validation = validateProjectName(input.name);
|
|
1365
|
+
if (!validation.valid) {
|
|
1366
|
+
throw new Error(validation.error);
|
|
1367
|
+
}
|
|
1368
|
+
const result = await this.client.CreateProject({ input });
|
|
1369
|
+
if (result.errors && result.errors.length > 0 && result.errors[0]) {
|
|
1370
|
+
throw new Error(result.errors[0].message);
|
|
1371
|
+
}
|
|
1372
|
+
if (!result.data?.createProject) {
|
|
1373
|
+
throw new Error("Failed to create project");
|
|
1374
|
+
}
|
|
1375
|
+
if (result.data.createProject.errors && result.data.createProject.errors.length > 0) {
|
|
1376
|
+
const firstError = result.data.createProject.errors[0];
|
|
1377
|
+
if (firstError) {
|
|
1378
|
+
throw new Error(firstError.message ?? "Validation failed");
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
project: result.data.createProject.project ? {
|
|
1383
|
+
name: result.data.createProject.project.name,
|
|
1384
|
+
defaultBranch: result.data.createProject.project.defaultBranch
|
|
1385
|
+
} : null,
|
|
1386
|
+
errors: result.data.createProject.errors?.map((e) => ({
|
|
1387
|
+
field: e?.field ?? "",
|
|
1388
|
+
message: e?.message ?? ""
|
|
1389
|
+
})) ?? null
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
detectPairedManifests(files) {
|
|
1393
|
+
if (files.length !== 2) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
const depsTreeFile = files.find((f) => f.filename === "deps-tree.txt");
|
|
1397
|
+
const depsFile = files.find((f) => f.filename === "deps.txt");
|
|
1398
|
+
if (depsTreeFile && depsFile) {
|
|
1399
|
+
return { projectFile: depsTreeFile, lockFile: depsFile };
|
|
1400
|
+
}
|
|
1401
|
+
const pyprojectFile = files.find((f) => f.filename === "pyproject.toml");
|
|
1402
|
+
const poetryLockFile = files.find((f) => f.filename === "poetry.lock");
|
|
1403
|
+
if (pyprojectFile && poetryLockFile) {
|
|
1404
|
+
return { projectFile: pyprojectFile, lockFile: poetryLockFile };
|
|
1405
|
+
}
|
|
1406
|
+
const pipfile = files.find((f) => f.filename === "Pipfile");
|
|
1407
|
+
const pipfileLock = files.find((f) => f.filename === "Pipfile.lock");
|
|
1408
|
+
if (pipfile && pipfileLock) {
|
|
1409
|
+
return { projectFile: pipfile, lockFile: pipfileLock };
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
async uploadManifests(params) {
|
|
1414
|
+
const { project, branch, label, files } = params;
|
|
1415
|
+
if (files.length === 0) {
|
|
1416
|
+
throw new Error("At least one file is required");
|
|
1417
|
+
}
|
|
1418
|
+
const formData = new FormData;
|
|
1419
|
+
formData.append("branch", branch);
|
|
1420
|
+
formData.append("label", label);
|
|
1421
|
+
const pairedManifests = this.detectPairedManifests(files);
|
|
1422
|
+
if (pairedManifests) {
|
|
1423
|
+
const { projectFile, lockFile } = pairedManifests;
|
|
1424
|
+
for (const file of [projectFile, lockFile]) {
|
|
1425
|
+
const sizeInBytes = new TextEncoder().encode(file.content).length;
|
|
1426
|
+
const sizeInMB = sizeInBytes / (1024 * 1024);
|
|
1427
|
+
if (sizeInMB > 10) {
|
|
1428
|
+
throw new Error(`File ${file.filename} exceeds 10MB limit (${sizeInMB.toFixed(2)}MB)`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const projectBlob = new Blob([projectFile.content], {
|
|
1432
|
+
type: "text/plain"
|
|
1433
|
+
});
|
|
1434
|
+
const lockBlob = new Blob([lockFile.content], { type: "text/plain" });
|
|
1435
|
+
formData.append("project_file", projectBlob, projectFile.filename);
|
|
1436
|
+
formData.append("lock_file", lockBlob, lockFile.filename);
|
|
1437
|
+
} else {
|
|
1438
|
+
for (const file of files) {
|
|
1439
|
+
const sizeInBytes = new TextEncoder().encode(file.content).length;
|
|
1440
|
+
const sizeInMB = sizeInBytes / (1024 * 1024);
|
|
1441
|
+
if (sizeInMB > 10) {
|
|
1442
|
+
throw new Error(`File ${file.filename} exceeds 10MB limit (${sizeInMB.toFixed(2)}MB)`);
|
|
1443
|
+
}
|
|
1444
|
+
const blob = new Blob([file.content], { type: "text/plain" });
|
|
1445
|
+
formData.append("files[]", blob, file.filename);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const url = `${this.baseUrl}/api/projects/${project}/manifests`;
|
|
1449
|
+
const response = await fetch(url, {
|
|
1450
|
+
method: "POST",
|
|
1451
|
+
headers: {
|
|
1452
|
+
Authorization: `Bearer ${this.apiToken}`
|
|
1453
|
+
},
|
|
1454
|
+
body: formData
|
|
1455
|
+
});
|
|
1456
|
+
if (!response.ok) {
|
|
1457
|
+
let errorMessage = `Failed to upload manifests: ${response.status} ${response.statusText}`;
|
|
1458
|
+
try {
|
|
1459
|
+
const responseClone = response.clone();
|
|
1460
|
+
const errorData = await responseClone.json();
|
|
1461
|
+
if (errorData.error?.message) {
|
|
1462
|
+
errorMessage = errorData.error.message;
|
|
1463
|
+
} else if (errorData.error?.code) {
|
|
1464
|
+
errorMessage = `Upload failed: ${errorData.error.code}`;
|
|
1465
|
+
}
|
|
1466
|
+
} catch {
|
|
1467
|
+
try {
|
|
1468
|
+
const responseClone = response.clone();
|
|
1469
|
+
const errorText = await responseClone.text();
|
|
1470
|
+
if (errorText) {
|
|
1471
|
+
errorMessage = `Failed to upload manifests: ${errorText}`;
|
|
1472
|
+
}
|
|
1473
|
+
} catch {
|
|
1474
|
+
errorMessage = `Failed to upload manifests: ${response.status} ${response.statusText}`;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (response.status === 401) {
|
|
1478
|
+
throw new Error("Authentication required. Please run `pkgseer login`.");
|
|
1479
|
+
}
|
|
1480
|
+
if (response.status === 403) {
|
|
1481
|
+
throw new Error("Insufficient permissions. Please run `pkgseer login` with proper scopes.");
|
|
1482
|
+
}
|
|
1483
|
+
if (response.status === 404) {
|
|
1484
|
+
throw new Error(`Project not found: ${project}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (response.status === 429) {
|
|
1487
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
1488
|
+
const retryMsg = retryAfter ? ` Please retry after ${retryAfter} seconds.` : "";
|
|
1489
|
+
throw new Error(`Upload rate limit exceeded.${retryMsg}`);
|
|
1490
|
+
}
|
|
1491
|
+
throw new Error(errorMessage);
|
|
1492
|
+
}
|
|
1493
|
+
return response.json();
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// src/services/prompt-service.ts
|
|
1497
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
1498
|
+
|
|
1499
|
+
class PromptServiceImpl {
|
|
1500
|
+
async input(message, defaultValue) {
|
|
1501
|
+
return input({ message, default: defaultValue });
|
|
1502
|
+
}
|
|
1503
|
+
async select(message, options) {
|
|
1504
|
+
return select({
|
|
1505
|
+
message,
|
|
1506
|
+
choices: options.map((opt) => ({
|
|
1507
|
+
value: opt.value,
|
|
1508
|
+
name: opt.name,
|
|
1509
|
+
description: opt.description
|
|
1510
|
+
}))
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async confirm(message, defaultValue) {
|
|
1514
|
+
return confirm({ message, default: defaultValue });
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
// src/services/shell-service.ts
|
|
1518
|
+
import { exec as exec2 } from "node:child_process";
|
|
1519
|
+
import { promisify as promisify2 } from "node:util";
|
|
1520
|
+
var execAsync2 = promisify2(exec2);
|
|
1521
|
+
|
|
1522
|
+
class ShellServiceImpl {
|
|
1523
|
+
async execute(command, cwd) {
|
|
1524
|
+
try {
|
|
1525
|
+
const { stdout } = await execAsync2(command, {
|
|
1526
|
+
cwd,
|
|
1527
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1528
|
+
});
|
|
1529
|
+
return stdout.trim();
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1532
|
+
throw new Error(`Command failed: ${command}
|
|
1533
|
+
Working directory: ${cwd}
|
|
1534
|
+
Error: ${errorMessage}`);
|
|
1535
|
+
}
|
|
597
1536
|
}
|
|
598
|
-
|
|
1537
|
+
}
|
|
1538
|
+
// src/container.ts
|
|
1539
|
+
async function resolveApiToken(configService, baseUrl) {
|
|
1540
|
+
const tokenData = await configService.getApiToken(baseUrl);
|
|
1541
|
+
return tokenData?.token;
|
|
599
1542
|
}
|
|
600
1543
|
async function createContainer() {
|
|
601
1544
|
const baseUrl = getBaseUrl();
|
|
602
1545
|
const fileSystemService = new FileSystemServiceImpl;
|
|
603
1546
|
const authStorage = new AuthStorageImpl(fileSystemService);
|
|
604
|
-
const
|
|
1547
|
+
const configService = new ConfigServiceImpl(fileSystemService, authStorage);
|
|
1548
|
+
const [apiToken, configResult] = await Promise.all([
|
|
1549
|
+
resolveApiToken(configService, baseUrl),
|
|
1550
|
+
configService.loadMergedConfig()
|
|
1551
|
+
]);
|
|
605
1552
|
const client = createClient(apiToken);
|
|
606
1553
|
return {
|
|
607
1554
|
pkgseerService: new PkgseerServiceImpl(client),
|
|
@@ -609,7 +1556,14 @@ async function createContainer() {
|
|
|
609
1556
|
authService: new AuthServiceImpl(baseUrl),
|
|
610
1557
|
browserService: new BrowserServiceImpl,
|
|
611
1558
|
fileSystemService,
|
|
1559
|
+
configService,
|
|
1560
|
+
projectService: new ProjectServiceImpl(client, baseUrl, apiToken ?? ""),
|
|
1561
|
+
gitService: new GitServiceImpl,
|
|
1562
|
+
promptService: new PromptServiceImpl,
|
|
1563
|
+
shellService: new ShellServiceImpl,
|
|
1564
|
+
config: configResult.config,
|
|
612
1565
|
baseUrl,
|
|
1566
|
+
apiToken,
|
|
613
1567
|
hasValidToken: apiToken !== undefined
|
|
614
1568
|
};
|
|
615
1569
|
}
|
|
@@ -663,151 +1617,2031 @@ function registerAuthStatusCommand(program) {
|
|
|
663
1617
|
});
|
|
664
1618
|
}
|
|
665
1619
|
|
|
666
|
-
// src/commands/
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
async function loginAction(options, deps) {
|
|
673
|
-
const { authService, authStorage, browserService, baseUrl } = deps;
|
|
674
|
-
const existing = await authStorage.load(baseUrl);
|
|
675
|
-
if (existing) {
|
|
676
|
-
const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
|
|
677
|
-
if (!isExpired) {
|
|
678
|
-
console.log(`Already logged in.
|
|
679
|
-
`);
|
|
680
|
-
console.log(` Environment: ${baseUrl}`);
|
|
681
|
-
console.log(` Token: ${existing.tokenName}
|
|
682
|
-
`);
|
|
683
|
-
console.log("To switch accounts, run `pkgseer logout` first.");
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
console.log(`Token expired. Starting new login...
|
|
687
|
-
`);
|
|
688
|
-
}
|
|
689
|
-
const { verifier, challenge, state } = authService.generatePkceParams();
|
|
690
|
-
const port = options.port ?? randomPort();
|
|
691
|
-
const authUrl = authService.buildAuthUrl({
|
|
692
|
-
state,
|
|
693
|
-
port,
|
|
694
|
-
codeChallenge: challenge,
|
|
695
|
-
hostname: hostname()
|
|
696
|
-
});
|
|
697
|
-
const serverPromise = authService.startCallbackServer(port);
|
|
698
|
-
if (options.browser === false) {
|
|
699
|
-
console.log(`Open this URL in your browser:
|
|
1620
|
+
// src/commands/config-show.ts
|
|
1621
|
+
async function configShowAction(deps) {
|
|
1622
|
+
const { configService } = deps;
|
|
1623
|
+
const { config, globalPath, projectPath } = await configService.loadMergedConfig();
|
|
1624
|
+
if (!globalPath && !projectPath) {
|
|
1625
|
+
console.log(`No configuration found.
|
|
700
1626
|
`);
|
|
701
|
-
console.log(
|
|
1627
|
+
console.log(" Global config: ~/.pkgseer/config.yml");
|
|
1628
|
+
console.log(` Project config: pkgseer.yml
|
|
702
1629
|
`);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
await browserService.open(authUrl);
|
|
1630
|
+
console.log("Create a config file to customize pkgseer behavior.");
|
|
1631
|
+
return;
|
|
706
1632
|
}
|
|
707
|
-
console.log(`
|
|
708
|
-
`);
|
|
709
|
-
let timeoutId;
|
|
710
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
711
|
-
timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
|
|
712
|
-
});
|
|
713
|
-
let callback;
|
|
714
|
-
try {
|
|
715
|
-
callback = await Promise.race([serverPromise, timeoutPromise]);
|
|
716
|
-
clearTimeout(timeoutId);
|
|
717
|
-
} catch (error) {
|
|
718
|
-
clearTimeout(timeoutId);
|
|
719
|
-
if (error instanceof Error) {
|
|
720
|
-
console.log(`${error.message}.
|
|
1633
|
+
console.log(`Configuration sources:
|
|
721
1634
|
`);
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
process.exit(1);
|
|
1635
|
+
if (globalPath) {
|
|
1636
|
+
console.log(` Global: ${globalPath}`);
|
|
725
1637
|
}
|
|
726
|
-
if (
|
|
727
|
-
console.
|
|
728
|
-
`);
|
|
729
|
-
console.log("This could indicate a security issue. Please try again.");
|
|
730
|
-
process.exit(1);
|
|
1638
|
+
if (projectPath) {
|
|
1639
|
+
console.log(` Project: ${projectPath}`);
|
|
731
1640
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
tokenResponse = await authService.exchangeCodeForToken({
|
|
735
|
-
code: callback.code,
|
|
736
|
-
codeVerifier: verifier,
|
|
737
|
-
state
|
|
738
|
-
});
|
|
739
|
-
} catch (error) {
|
|
740
|
-
console.error(`Failed to complete authentication: ${error instanceof Error ? error.message : error}
|
|
1641
|
+
console.log(`
|
|
1642
|
+
Merged configuration:
|
|
741
1643
|
`);
|
|
742
|
-
|
|
743
|
-
|
|
1644
|
+
if (config.enabled_tools !== undefined) {
|
|
1645
|
+
if (config.enabled_tools.length > 0) {
|
|
1646
|
+
console.log(" enabled_tools:");
|
|
1647
|
+
for (const tool of config.enabled_tools) {
|
|
1648
|
+
console.log(` - ${tool}`);
|
|
1649
|
+
}
|
|
1650
|
+
} else {
|
|
1651
|
+
console.log(" enabled_tools: [] (no tools enabled)");
|
|
1652
|
+
}
|
|
1653
|
+
} else {
|
|
1654
|
+
console.log(" enabled_tools: (all tools enabled by default)");
|
|
744
1655
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
tokenName: tokenResponse.tokenName,
|
|
748
|
-
scopes: tokenResponse.scopes,
|
|
749
|
-
createdAt: new Date().toISOString(),
|
|
750
|
-
expiresAt: tokenResponse.expiresAt,
|
|
751
|
-
apiKeyId: tokenResponse.apiKeyId
|
|
752
|
-
});
|
|
753
|
-
console.log(`✓ Logged in
|
|
754
|
-
`);
|
|
755
|
-
console.log(` Environment: ${baseUrl}`);
|
|
756
|
-
console.log(` Token: ${tokenResponse.tokenName}`);
|
|
757
|
-
if (tokenResponse.expiresAt) {
|
|
758
|
-
const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
759
|
-
console.log(` Expires: in ${days} days`);
|
|
1656
|
+
if (config.project) {
|
|
1657
|
+
console.log(` project: ${config.project}`);
|
|
760
1658
|
}
|
|
761
|
-
console.log(`
|
|
762
|
-
You're ready to use pkgseer with your AI assistant.`);
|
|
763
1659
|
}
|
|
764
|
-
var
|
|
765
|
-
|
|
766
|
-
Opens your browser to complete authentication securely. The CLI receives
|
|
767
|
-
a token that's stored locally and used for API requests.
|
|
1660
|
+
var SHOW_DESCRIPTION = `Display current configuration.
|
|
768
1661
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1662
|
+
Shows the merged configuration from global (~/.pkgseer/config.yml) and
|
|
1663
|
+
project (pkgseer.yml) config files. Project config takes precedence
|
|
1664
|
+
over global config for overlapping settings.`;
|
|
1665
|
+
function registerConfigShowCommand(program) {
|
|
1666
|
+
program.command("show").summary("Display current configuration").description(SHOW_DESCRIPTION).action(async () => {
|
|
773
1667
|
const deps = await createContainer();
|
|
774
|
-
await
|
|
1668
|
+
await configShowAction(deps);
|
|
775
1669
|
});
|
|
776
1670
|
}
|
|
777
1671
|
|
|
778
|
-
// src/commands/
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1672
|
+
// src/commands/shared.ts
|
|
1673
|
+
function toGraphQLRegistry(registry) {
|
|
1674
|
+
const map = {
|
|
1675
|
+
npm: "NPM",
|
|
1676
|
+
pypi: "PYPI",
|
|
1677
|
+
hex: "HEX"
|
|
1678
|
+
};
|
|
1679
|
+
return map[registry.toLowerCase()] || "NPM";
|
|
1680
|
+
}
|
|
1681
|
+
function output(data, json) {
|
|
1682
|
+
if (json) {
|
|
1683
|
+
console.log(JSON.stringify(data));
|
|
1684
|
+
} else {
|
|
1685
|
+
console.log(data);
|
|
787
1686
|
}
|
|
788
|
-
try {
|
|
789
|
-
await authService.revokeToken(auth.token);
|
|
790
|
-
} catch {}
|
|
791
|
-
await authStorage.clear(baseUrl);
|
|
792
|
-
console.log(`✓ Logged out
|
|
793
|
-
`);
|
|
794
|
-
console.log(` Environment: ${baseUrl}`);
|
|
795
1687
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
await logoutAction(deps);
|
|
804
|
-
});
|
|
1688
|
+
function outputError(message, json) {
|
|
1689
|
+
if (json) {
|
|
1690
|
+
console.error(JSON.stringify({ error: message }));
|
|
1691
|
+
} else {
|
|
1692
|
+
console.error(`Error: ${message}`);
|
|
1693
|
+
}
|
|
1694
|
+
process.exit(1);
|
|
805
1695
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1696
|
+
function handleErrors(errors, json) {
|
|
1697
|
+
if (errors && errors.length > 0) {
|
|
1698
|
+
const message = errors.map((e) => e.message).join(", ");
|
|
1699
|
+
outputError(message, json);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function formatNumber(num) {
|
|
1703
|
+
if (num == null)
|
|
1704
|
+
return "N/A";
|
|
1705
|
+
return num.toLocaleString();
|
|
1706
|
+
}
|
|
1707
|
+
function keyValueTable(pairs) {
|
|
1708
|
+
const maxKeyLen = Math.max(...pairs.map(([k]) => k.length));
|
|
1709
|
+
return pairs.map(([key, value]) => ` ${key.padEnd(maxKeyLen)} ${value ?? "N/A"}`).join(`
|
|
1710
|
+
`);
|
|
1711
|
+
}
|
|
1712
|
+
function formatSeverity(severity) {
|
|
1713
|
+
const indicators = {
|
|
1714
|
+
CRITICAL: "\uD83D\uDD34 CRITICAL",
|
|
1715
|
+
HIGH: "\uD83D\uDFE0 HIGH",
|
|
1716
|
+
MEDIUM: "\uD83D\uDFE1 MEDIUM",
|
|
1717
|
+
LOW: "\uD83D\uDFE2 LOW",
|
|
1718
|
+
UNKNOWN: "⚪ UNKNOWN"
|
|
1719
|
+
};
|
|
1720
|
+
return indicators[severity] || severity;
|
|
1721
|
+
}
|
|
1722
|
+
function extractGraphQLError(error) {
|
|
1723
|
+
if (error && typeof error === "object" && "response" in error && error.response && typeof error.response === "object" && "errors" in error.response && Array.isArray(error.response.errors)) {
|
|
1724
|
+
const errors = error.response.errors;
|
|
1725
|
+
if (errors.length > 0) {
|
|
1726
|
+
const messages = errors.map((e) => e.message).filter((m) => typeof m === "string");
|
|
1727
|
+
if (messages.length > 0) {
|
|
1728
|
+
return messages.join("; ");
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (error && typeof error === "object" && "errors" in error && Array.isArray(error.errors)) {
|
|
1733
|
+
const errors = error.errors;
|
|
1734
|
+
if (errors.length > 0) {
|
|
1735
|
+
const messages = errors.map((e) => e.message).filter((m) => typeof m === "string");
|
|
1736
|
+
if (messages.length > 0) {
|
|
1737
|
+
return messages.join("; ");
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (error instanceof Error) {
|
|
1742
|
+
const message = error.message;
|
|
1743
|
+
const jsonMatch = message.match(/"message"\s*:\s*"([^"]+)"/);
|
|
1744
|
+
if (jsonMatch?.[1]) {
|
|
1745
|
+
return jsonMatch[1];
|
|
1746
|
+
}
|
|
1747
|
+
if (message.includes("Cannot query field") || message.includes("Unknown field")) {
|
|
1748
|
+
return message;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
function formatErrorMessage(errorMessage, isJson) {
|
|
1754
|
+
if (isJson) {
|
|
1755
|
+
return errorMessage;
|
|
1756
|
+
}
|
|
1757
|
+
if (errorMessage.includes("Cannot query field") || errorMessage.includes("Unknown field") || errorMessage.includes("Field") && errorMessage.includes("doesn't exist")) {
|
|
1758
|
+
return `${errorMessage}
|
|
1759
|
+
|
|
1760
|
+
` + `This error usually indicates a schema mismatch. Possible causes:
|
|
1761
|
+
` + ` - The server schema is outdated (try updating the server)
|
|
1762
|
+
` + ` - The client schema is outdated (run: bun run codegen)
|
|
1763
|
+
` + ` - You're querying a different server than expected (check PKGSEER_URL)`;
|
|
1764
|
+
}
|
|
1765
|
+
return errorMessage;
|
|
1766
|
+
}
|
|
1767
|
+
async function withCliErrorHandling(json, fn) {
|
|
1768
|
+
try {
|
|
1769
|
+
await fn();
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
const graphQLError = extractGraphQLError(error);
|
|
1772
|
+
if (graphQLError) {
|
|
1773
|
+
const formatted = formatErrorMessage(graphQLError, json);
|
|
1774
|
+
outputError(formatted, json);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1778
|
+
const colonMatch = message.match(/^([^:]+):/);
|
|
1779
|
+
const shortMessage = colonMatch?.[1] ?? message;
|
|
1780
|
+
outputError(shortMessage, json);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function formatScore(score) {
|
|
1784
|
+
if (score == null)
|
|
1785
|
+
return "N/A";
|
|
1786
|
+
const percentage = score > 1 ? Math.round(score) : Math.round(score * 100);
|
|
1787
|
+
const filled = Math.round(percentage / 5);
|
|
1788
|
+
const bar = "█".repeat(Math.min(filled, 20)) + "░".repeat(Math.max(20 - filled, 0));
|
|
1789
|
+
return `${bar} ${percentage}%`;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// src/commands/docs/get.ts
|
|
1793
|
+
function parsePackageRef(ref) {
|
|
1794
|
+
if (!ref.includes("/")) {
|
|
1795
|
+
throw new Error(`Invalid format: "${ref}". Expected format:
|
|
1796
|
+
` + ` Full form: <registry>/<package>/<version>/<document>
|
|
1797
|
+
` + ` Short form: <package>/<document>
|
|
1798
|
+
` + `Examples: npm/express/4.18.2/readme or express/readme`);
|
|
1799
|
+
}
|
|
1800
|
+
const parts = ref.split("/");
|
|
1801
|
+
if (parts.length < 2) {
|
|
1802
|
+
throw new Error(`Invalid format: "${ref}". Expected at least 2 parts separated by "/"`);
|
|
1803
|
+
}
|
|
1804
|
+
if (parts.length >= 4) {
|
|
1805
|
+
const registry = parts[0];
|
|
1806
|
+
const packageName2 = parts[1];
|
|
1807
|
+
const version2 = parts[2];
|
|
1808
|
+
const pageId2 = parts.slice(3).join("/");
|
|
1809
|
+
if (!registry || !packageName2 || !version2 || !pageId2) {
|
|
1810
|
+
throw new Error(`Invalid full form: "${ref}". All components (registry/package/version/document) are required.`);
|
|
1811
|
+
}
|
|
1812
|
+
return {
|
|
1813
|
+
registry: registry.toLowerCase(),
|
|
1814
|
+
packageName: packageName2,
|
|
1815
|
+
version: version2,
|
|
1816
|
+
pageId: pageId2,
|
|
1817
|
+
originalRef: ref
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const packageName = parts[0];
|
|
1821
|
+
const pageId = parts.slice(1).join("/");
|
|
1822
|
+
if (!packageName || !pageId) {
|
|
1823
|
+
throw new Error(`Invalid format: "${ref}". Expected format: <package>/<document>`);
|
|
1824
|
+
}
|
|
1825
|
+
return { packageName, pageId, originalRef: ref };
|
|
1826
|
+
}
|
|
1827
|
+
function formatDocPage(data, ref) {
|
|
1828
|
+
const lines = [];
|
|
1829
|
+
const page = data.page;
|
|
1830
|
+
if (!page) {
|
|
1831
|
+
return `[${ref}] No page content available.`;
|
|
1832
|
+
}
|
|
1833
|
+
lines.push(`[${ref}]`);
|
|
1834
|
+
lines.push("");
|
|
1835
|
+
if (page.breadcrumbs && page.breadcrumbs.length > 0) {
|
|
1836
|
+
lines.push(page.breadcrumbs.join(" > "));
|
|
1837
|
+
lines.push("");
|
|
1838
|
+
}
|
|
1839
|
+
lines.push(`# ${page.title}`);
|
|
1840
|
+
lines.push("");
|
|
1841
|
+
if (page.content) {
|
|
1842
|
+
lines.push(page.content);
|
|
1843
|
+
} else {
|
|
1844
|
+
lines.push("(No content available)");
|
|
1845
|
+
}
|
|
1846
|
+
return lines.join(`
|
|
1847
|
+
`);
|
|
1848
|
+
}
|
|
1849
|
+
function extractErrorMessage(error) {
|
|
1850
|
+
if (error instanceof Error) {
|
|
1851
|
+
const message = error.message;
|
|
1852
|
+
if (message.includes('"response"')) {
|
|
1853
|
+
const colonIndex = message.indexOf(":");
|
|
1854
|
+
if (colonIndex > 0) {
|
|
1855
|
+
return message.slice(0, colonIndex).trim();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return message;
|
|
1859
|
+
}
|
|
1860
|
+
return "Unknown error occurred";
|
|
1861
|
+
}
|
|
1862
|
+
async function fetchPage(ref, defaultRegistry, defaultVersion, pkgseerService) {
|
|
1863
|
+
try {
|
|
1864
|
+
const registry = ref.registry ? toGraphQLRegistry(ref.registry) : defaultRegistry;
|
|
1865
|
+
const version2 = ref.version ?? defaultVersion;
|
|
1866
|
+
const result = await pkgseerService.cliDocsGet(registry, ref.packageName, ref.pageId, version2);
|
|
1867
|
+
if (result.errors && result.errors.length > 0) {
|
|
1868
|
+
const errorMsg = result.errors.map((e) => {
|
|
1869
|
+
if (typeof e === "object" && e !== null && "message" in e) {
|
|
1870
|
+
return String(e.message);
|
|
1871
|
+
}
|
|
1872
|
+
return String(e);
|
|
1873
|
+
}).join("; ");
|
|
1874
|
+
return { ref: ref.originalRef, result: null, error: errorMsg };
|
|
1875
|
+
}
|
|
1876
|
+
if (!result.data.fetchPackageDoc) {
|
|
1877
|
+
return {
|
|
1878
|
+
ref: ref.originalRef,
|
|
1879
|
+
result: null,
|
|
1880
|
+
error: `Page not found: ${ref.pageId} for ${ref.packageName}`
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
return { ref: ref.originalRef, result: result.data.fetchPackageDoc };
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
return {
|
|
1886
|
+
ref: ref.originalRef,
|
|
1887
|
+
result: null,
|
|
1888
|
+
error: extractErrorMessage(error)
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async function docsGetAction(refs, options, deps) {
|
|
1893
|
+
if (refs.length === 0) {
|
|
1894
|
+
outputError("At least one page reference required. Format: <package>/<slug>", options.json ?? false);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const { pkgseerService } = deps;
|
|
1898
|
+
const defaultRegistry = toGraphQLRegistry(options.registry);
|
|
1899
|
+
const parsedRefs = [];
|
|
1900
|
+
for (const ref of refs) {
|
|
1901
|
+
try {
|
|
1902
|
+
parsedRefs.push(parsePackageRef(ref));
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1905
|
+
outputError(`Invalid reference "${ref}": ${message}`, options.json ?? false);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
const results = await Promise.all(parsedRefs.map((ref) => fetchPage(ref, defaultRegistry, options.pkgVersion, pkgseerService)));
|
|
1910
|
+
const errors = results.filter((r) => r.error);
|
|
1911
|
+
const successes = results.filter((r) => r.result);
|
|
1912
|
+
if (errors.length === results.length) {
|
|
1913
|
+
const errorMessages = errors.map((e) => ` ${e.ref}: ${e.error}`).join(`
|
|
1914
|
+
`);
|
|
1915
|
+
outputError(`Failed to fetch all pages:
|
|
1916
|
+
${errorMessages}`, options.json ?? false);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (errors.length > 0 && !options.json) {
|
|
1920
|
+
for (const { ref, error } of errors) {
|
|
1921
|
+
console.error(`Error fetching ${ref}: ${error}`);
|
|
1922
|
+
}
|
|
1923
|
+
if (successes.length > 0) {
|
|
1924
|
+
console.error("");
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (options.json) {
|
|
1928
|
+
const jsonResults = results.map((r) => {
|
|
1929
|
+
if (r.error) {
|
|
1930
|
+
return { ref: r.ref, error: r.error };
|
|
1931
|
+
}
|
|
1932
|
+
const page = r.result?.page;
|
|
1933
|
+
return {
|
|
1934
|
+
ref: r.ref,
|
|
1935
|
+
title: page?.title,
|
|
1936
|
+
content: page?.content
|
|
1937
|
+
};
|
|
1938
|
+
});
|
|
1939
|
+
output(jsonResults, true);
|
|
1940
|
+
} else {
|
|
1941
|
+
if (successes.length > 0) {
|
|
1942
|
+
const pages = successes.filter((r) => r.result !== null).map((r) => formatDocPage(r.result, r.ref));
|
|
1943
|
+
console.log(pages.join(`
|
|
1944
|
+
|
|
1945
|
+
---
|
|
1946
|
+
|
|
1947
|
+
`));
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (errors.length > 0) {
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
var GET_DESCRIPTION = `Fetch one or more documentation pages.
|
|
1955
|
+
|
|
1956
|
+
Retrieves the full content of documentation pages including
|
|
1957
|
+
title, breadcrumbs, content (markdown), and source URL.
|
|
1958
|
+
|
|
1959
|
+
Use 'pkgseer docs list' first to discover available page IDs, or
|
|
1960
|
+
use the output format from 'pkgseer docs search --refs-only'.
|
|
1961
|
+
|
|
1962
|
+
Format: <registry>/<package>/<version>/<document> (full form)
|
|
1963
|
+
or <package>/<document> (short form, requires --registry/--pkg-version)
|
|
1964
|
+
|
|
1965
|
+
Examples:
|
|
1966
|
+
# Full form (from search output)
|
|
1967
|
+
pkgseer docs get npm/express/4.18.2/readme
|
|
1968
|
+
pkgseer docs get hex/postgrex/1.15.0/readme
|
|
1969
|
+
|
|
1970
|
+
# Short form (backward compatibility)
|
|
1971
|
+
pkgseer docs get express/readme --registry npm --pkg-version 4.18.2
|
|
1972
|
+
pkgseer docs get postgrex/readme --registry hex
|
|
1973
|
+
|
|
1974
|
+
# Multiple pages
|
|
1975
|
+
pkgseer docs get npm/express/4.18.2/readme npm/express/4.18.2/writing-middleware
|
|
1976
|
+
|
|
1977
|
+
# Pipe from search (full form works seamlessly)
|
|
1978
|
+
pkgseer docs search log --package express --refs-only | \\
|
|
1979
|
+
xargs pkgseer docs get`;
|
|
1980
|
+
function registerDocsGetCommand(program) {
|
|
1981
|
+
program.command("get <refs...>").summary("Fetch documentation page(s)").description(GET_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (refs, options) => {
|
|
1982
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
1983
|
+
const deps = await createContainer();
|
|
1984
|
+
await docsGetAction(refs, options, deps);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
// src/commands/docs/list.ts
|
|
1989
|
+
function formatDocsList(docs) {
|
|
1990
|
+
const lines = [];
|
|
1991
|
+
lines.push(`\uD83D\uDCDA Documentation: ${docs.packageName}@${docs.version}`);
|
|
1992
|
+
lines.push("");
|
|
1993
|
+
if (!docs.pages || docs.pages.length === 0) {
|
|
1994
|
+
lines.push("No documentation pages found.");
|
|
1995
|
+
return lines.join(`
|
|
1996
|
+
`);
|
|
1997
|
+
}
|
|
1998
|
+
lines.push(`Found ${docs.pages.length} pages:`);
|
|
1999
|
+
lines.push("");
|
|
2000
|
+
for (const page of docs.pages) {
|
|
2001
|
+
if (!page)
|
|
2002
|
+
continue;
|
|
2003
|
+
const words = page.words ? ` (${formatNumber(page.words)} words)` : "";
|
|
2004
|
+
lines.push(` ${page.title}${words}`);
|
|
2005
|
+
lines.push(` ID: ${page.slug}`);
|
|
2006
|
+
lines.push("");
|
|
2007
|
+
}
|
|
2008
|
+
lines.push("Use 'pkgseer docs get <package>/<page-id>' to fetch a page.");
|
|
2009
|
+
return lines.join(`
|
|
2010
|
+
`);
|
|
2011
|
+
}
|
|
2012
|
+
async function docsListAction(packageName, options, deps) {
|
|
2013
|
+
const { pkgseerService } = deps;
|
|
2014
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
2015
|
+
const result = await pkgseerService.cliDocsList(registry, packageName, options.pkgVersion);
|
|
2016
|
+
handleErrors(result.errors, options.json ?? false);
|
|
2017
|
+
if (!result.data.listPackageDocs) {
|
|
2018
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (options.json) {
|
|
2022
|
+
const pages = result.data.listPackageDocs.pages?.filter((p) => p) ?? [];
|
|
2023
|
+
const slim = pages.map((p) => ({
|
|
2024
|
+
slug: p.slug,
|
|
2025
|
+
title: p.title
|
|
2026
|
+
}));
|
|
2027
|
+
output(slim, true);
|
|
2028
|
+
} else {
|
|
2029
|
+
console.log(formatDocsList(result.data.listPackageDocs));
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
var LIST_DESCRIPTION = `List available documentation pages for a package.
|
|
2033
|
+
|
|
2034
|
+
Shows all documentation pages with titles, page IDs (slugs),
|
|
2035
|
+
word counts, and descriptions. Use the page ID with 'docs get'
|
|
2036
|
+
to fetch the full content of a specific page.
|
|
2037
|
+
|
|
2038
|
+
Examples:
|
|
2039
|
+
pkgseer docs list lodash
|
|
2040
|
+
pkgseer docs list requests --registry pypi
|
|
2041
|
+
pkgseer docs list phoenix --registry hex --version 1.7.0 --json`;
|
|
2042
|
+
function registerDocsListCommand(program) {
|
|
2043
|
+
program.command("list <package>").summary("List documentation pages").description(LIST_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
|
|
2044
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
2045
|
+
const deps = await createContainer();
|
|
2046
|
+
await docsListAction(packageName, options, deps);
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
// src/commands/docs/search.ts
|
|
2051
|
+
function buildDocRef(entry, searchResult) {
|
|
2052
|
+
if (!entry?.slug)
|
|
2053
|
+
return "";
|
|
2054
|
+
let registry;
|
|
2055
|
+
let packageName;
|
|
2056
|
+
let version2;
|
|
2057
|
+
if ("packageName" in entry && "registry" in entry && "version" in entry) {
|
|
2058
|
+
registry = entry.registry;
|
|
2059
|
+
packageName = entry.packageName;
|
|
2060
|
+
version2 = entry.version;
|
|
2061
|
+
} else {
|
|
2062
|
+
const rawRegistry = "registry" in searchResult ? searchResult.registry : null;
|
|
2063
|
+
registry = rawRegistry ? rawRegistry.toLowerCase() : null;
|
|
2064
|
+
packageName = "packageName" in searchResult ? searchResult.packageName : null;
|
|
2065
|
+
version2 = "version" in searchResult ? searchResult.version : null;
|
|
2066
|
+
}
|
|
2067
|
+
const parts = [];
|
|
2068
|
+
if (registry)
|
|
2069
|
+
parts.push(registry.toLowerCase());
|
|
2070
|
+
if (packageName)
|
|
2071
|
+
parts.push(packageName);
|
|
2072
|
+
if (version2)
|
|
2073
|
+
parts.push(version2);
|
|
2074
|
+
else if (registry && packageName)
|
|
2075
|
+
parts.push("latest");
|
|
2076
|
+
if (entry.slug)
|
|
2077
|
+
parts.push(entry.slug);
|
|
2078
|
+
return parts.join("/");
|
|
2079
|
+
}
|
|
2080
|
+
var colors = {
|
|
2081
|
+
reset: "\x1B[0m",
|
|
2082
|
+
bold: "\x1B[1m",
|
|
2083
|
+
dim: "\x1B[2m",
|
|
2084
|
+
magenta: "\x1B[35m",
|
|
2085
|
+
cyan: "\x1B[36m",
|
|
2086
|
+
yellow: "\x1B[33m",
|
|
2087
|
+
green: "\x1B[32m"
|
|
2088
|
+
};
|
|
2089
|
+
function shouldUseColors(noColor) {
|
|
2090
|
+
if (noColor)
|
|
2091
|
+
return false;
|
|
2092
|
+
if (process.env.NO_COLOR !== undefined)
|
|
2093
|
+
return false;
|
|
2094
|
+
return process.stdout.isTTY ?? false;
|
|
2095
|
+
}
|
|
2096
|
+
function applyHighlights(line, highlights, useColors) {
|
|
2097
|
+
if (!highlights || highlights.length === 0 || !useColors) {
|
|
2098
|
+
return line;
|
|
2099
|
+
}
|
|
2100
|
+
const sorted = [...highlights].filter((h) => h && h.start != null && h.end != null).sort((a, b) => {
|
|
2101
|
+
const aStart = a?.start ?? 0;
|
|
2102
|
+
const bStart = b?.start ?? 0;
|
|
2103
|
+
return bStart - aStart;
|
|
2104
|
+
});
|
|
2105
|
+
let result = line;
|
|
2106
|
+
for (const h of sorted) {
|
|
2107
|
+
if (!h || h.start == null || h.end == null)
|
|
2108
|
+
continue;
|
|
2109
|
+
const before = result.slice(0, h.start);
|
|
2110
|
+
const match = result.slice(h.start, h.end);
|
|
2111
|
+
const after = result.slice(h.end);
|
|
2112
|
+
result = `${before}${colors.bold}${colors.magenta}${match}${colors.reset}${after}`;
|
|
2113
|
+
}
|
|
2114
|
+
return result;
|
|
2115
|
+
}
|
|
2116
|
+
function formatEntry(entry, searchResult, useColors) {
|
|
2117
|
+
if (!entry)
|
|
2118
|
+
return "";
|
|
2119
|
+
const lines = [];
|
|
2120
|
+
const ref = buildDocRef(entry, searchResult);
|
|
2121
|
+
const matchInfo = entry.matchCount && entry.matchCount > 0 ? ` (${entry.matchCount} match${entry.matchCount > 1 ? "es" : ""})` : "";
|
|
2122
|
+
if (useColors) {
|
|
2123
|
+
lines.push(`${colors.cyan}${ref}${colors.reset}: ${colors.bold}${entry.title}${colors.reset}${colors.dim}${matchInfo}${colors.reset}`);
|
|
2124
|
+
} else {
|
|
2125
|
+
lines.push(`${ref}: ${entry.title}${matchInfo}`);
|
|
2126
|
+
}
|
|
2127
|
+
const matches = entry.matches ?? [];
|
|
2128
|
+
for (const match of matches) {
|
|
2129
|
+
if (!match?.context)
|
|
2130
|
+
continue;
|
|
2131
|
+
const ctx = match.context;
|
|
2132
|
+
for (const beforeLine of ctx.before ?? []) {
|
|
2133
|
+
if (beforeLine) {
|
|
2134
|
+
const prefix = useColors ? colors.dim : "";
|
|
2135
|
+
const suffix = useColors ? colors.reset : "";
|
|
2136
|
+
lines.push(` ${prefix}${beforeLine}${suffix}`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
for (const matchedLine of ctx.matchedLines ?? []) {
|
|
2140
|
+
if (matchedLine?.content) {
|
|
2141
|
+
const highlighted = applyHighlights(matchedLine.content, matchedLine.highlights, useColors);
|
|
2142
|
+
lines.push(` ${highlighted}`);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
for (const afterLine of ctx.after ?? []) {
|
|
2146
|
+
if (afterLine) {
|
|
2147
|
+
const prefix = useColors ? colors.dim : "";
|
|
2148
|
+
const suffix = useColors ? colors.reset : "";
|
|
2149
|
+
lines.push(` ${prefix}${afterLine}${suffix}`);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
lines.push("");
|
|
2153
|
+
}
|
|
2154
|
+
return lines.join(`
|
|
2155
|
+
`);
|
|
2156
|
+
}
|
|
2157
|
+
function formatSearchResults(results, useColors) {
|
|
2158
|
+
const entries = results.entries ?? [];
|
|
2159
|
+
if (entries.length === 0) {
|
|
2160
|
+
return "No matching documentation found.";
|
|
2161
|
+
}
|
|
2162
|
+
const formatted = entries.filter((e) => e != null).map((entry) => formatEntry(entry, results, useColors));
|
|
2163
|
+
return formatted.join(`
|
|
2164
|
+
`);
|
|
2165
|
+
}
|
|
2166
|
+
function formatRefsOnly(results) {
|
|
2167
|
+
const entries = results.entries ?? [];
|
|
2168
|
+
return entries.filter((e) => e != null).map((entry) => buildDocRef(entry, results)).join(`
|
|
2169
|
+
`);
|
|
2170
|
+
}
|
|
2171
|
+
function formatCount(results) {
|
|
2172
|
+
const entries = results.entries ?? [];
|
|
2173
|
+
return entries.filter((e) => e != null).map((entry) => {
|
|
2174
|
+
if (!entry)
|
|
2175
|
+
return "";
|
|
2176
|
+
const ref = buildDocRef(entry, results);
|
|
2177
|
+
return `${ref}: ${entry.matchCount ?? 0}`;
|
|
2178
|
+
}).filter((s) => s !== "").join(`
|
|
2179
|
+
`);
|
|
2180
|
+
}
|
|
2181
|
+
function slimSearchResults(results) {
|
|
2182
|
+
const entries = results.entries ?? [];
|
|
2183
|
+
return entries.filter((e) => e != null).map((entry) => {
|
|
2184
|
+
if (!entry)
|
|
2185
|
+
return null;
|
|
2186
|
+
return {
|
|
2187
|
+
ref: buildDocRef(entry, results),
|
|
2188
|
+
title: entry.title ?? "",
|
|
2189
|
+
matchCount: entry.matchCount ?? 0,
|
|
2190
|
+
matches: entry.matches?.filter((m) => m?.context).map((m) => {
|
|
2191
|
+
if (!m?.context)
|
|
2192
|
+
return null;
|
|
2193
|
+
return {
|
|
2194
|
+
before: (m.context.before ?? []).filter((l) => l != null),
|
|
2195
|
+
lines: (m.context.matchedLines ?? []).filter((l) => l?.content).map((l) => l?.content ?? ""),
|
|
2196
|
+
after: (m.context.after ?? []).filter((l) => l != null)
|
|
2197
|
+
};
|
|
2198
|
+
}).filter((m) => m != null)
|
|
2199
|
+
};
|
|
2200
|
+
}).filter((e) => e != null);
|
|
2201
|
+
}
|
|
2202
|
+
async function docsSearchAction(queryArg, options, deps) {
|
|
2203
|
+
const { pkgseerService, config } = deps;
|
|
2204
|
+
const searchQuery = queryArg || options.query;
|
|
2205
|
+
const keywords = options.keywords;
|
|
2206
|
+
if (!searchQuery && (!keywords || keywords.length === 0)) {
|
|
2207
|
+
outputError("Search query required. Provide a query as argument or use --query/--keywords", options.json ?? false);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const contextBefore = options.context ? Number.parseInt(options.context, 10) : options.before ? Number.parseInt(options.before, 10) : 2;
|
|
2211
|
+
const contextAfter = options.context ? Number.parseInt(options.context, 10) : options.after ? Number.parseInt(options.after, 10) : 2;
|
|
2212
|
+
const maxMatches = options.maxMatches ? Number.parseInt(options.maxMatches, 10) : 5;
|
|
2213
|
+
const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
|
|
2214
|
+
const matchMode = options.mode?.toUpperCase() || undefined;
|
|
2215
|
+
const useColors = shouldUseColors(options.noColor);
|
|
2216
|
+
const project = options.project ?? config.project;
|
|
2217
|
+
const packageName = options.package;
|
|
2218
|
+
if (packageName) {
|
|
2219
|
+
const registry = toGraphQLRegistry(options.registry ?? "npm");
|
|
2220
|
+
const result = await pkgseerService.cliDocsSearch(registry, packageName, {
|
|
2221
|
+
query: searchQuery,
|
|
2222
|
+
keywords,
|
|
2223
|
+
matchMode,
|
|
2224
|
+
limit,
|
|
2225
|
+
version: options.pkgVersion,
|
|
2226
|
+
contextLinesBefore: contextBefore,
|
|
2227
|
+
contextLinesAfter: contextAfter,
|
|
2228
|
+
maxMatches
|
|
2229
|
+
});
|
|
2230
|
+
handleErrors(result.errors, options.json ?? false);
|
|
2231
|
+
if (!result.data.searchPackageDocs) {
|
|
2232
|
+
outputError(`No documentation found for ${packageName} in ${options.registry ?? "npm"}`, options.json ?? false);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
outputResults(result.data.searchPackageDocs, options, useColors);
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (project) {
|
|
2239
|
+
const result = await pkgseerService.cliProjectDocsSearch(project, {
|
|
2240
|
+
query: searchQuery,
|
|
2241
|
+
keywords,
|
|
2242
|
+
matchMode,
|
|
2243
|
+
limit,
|
|
2244
|
+
contextLinesBefore: contextBefore,
|
|
2245
|
+
contextLinesAfter: contextAfter,
|
|
2246
|
+
maxMatches
|
|
2247
|
+
});
|
|
2248
|
+
handleErrors(result.errors, options.json ?? false);
|
|
2249
|
+
if (!result.data.searchProjectDocs) {
|
|
2250
|
+
outputError(`Project not found: ${project}`, options.json ?? false);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
outputResults(result.data.searchProjectDocs, options, useColors);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
outputError(`No project configured. Either:
|
|
2257
|
+
` + ` - Add project to pkgseer.yml
|
|
2258
|
+
` + ` - Use --project <name> to specify a project
|
|
2259
|
+
` + " - Use --package <name> to search a specific package", options.json ?? false);
|
|
2260
|
+
}
|
|
2261
|
+
function outputResults(results, options, useColors) {
|
|
2262
|
+
if (options.json) {
|
|
2263
|
+
output(slimSearchResults(results), true);
|
|
2264
|
+
} else if (options.refsOnly) {
|
|
2265
|
+
console.log(formatRefsOnly(results));
|
|
2266
|
+
} else if (options.count) {
|
|
2267
|
+
console.log(formatCount(results));
|
|
2268
|
+
} else {
|
|
2269
|
+
console.log(formatSearchResults(results, useColors));
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
var SEARCH_DESCRIPTION = `Search documentation with grep-like output.
|
|
2273
|
+
|
|
2274
|
+
Searches across package or project documentation with keyword
|
|
2275
|
+
or freeform query support. Shows matching lines with context
|
|
2276
|
+
and highlights matched terms.
|
|
2277
|
+
|
|
2278
|
+
By default, searches project documentation if project is
|
|
2279
|
+
configured in pkgseer.yml. Use --package to search a specific
|
|
2280
|
+
package instead.
|
|
2281
|
+
|
|
2282
|
+
Examples:
|
|
2283
|
+
# Search with context (like grep)
|
|
2284
|
+
pkgseer docs search "error handling" --package express
|
|
2285
|
+
pkgseer docs search log --package express -C 3
|
|
2286
|
+
|
|
2287
|
+
# Multiple keywords (OR by default)
|
|
2288
|
+
pkgseer docs search -k "middleware,routing" --package express
|
|
2289
|
+
|
|
2290
|
+
# Strict matching (AND mode)
|
|
2291
|
+
pkgseer docs search -k "error,middleware" --mode and --package express
|
|
2292
|
+
|
|
2293
|
+
# Output for piping
|
|
2294
|
+
pkgseer docs search log --package express --refs-only | \\
|
|
2295
|
+
xargs -I{} pkgseer docs get express {}
|
|
2296
|
+
|
|
2297
|
+
# Count matches
|
|
2298
|
+
pkgseer docs search error --package express --count`;
|
|
2299
|
+
function addSearchOptions(cmd) {
|
|
2300
|
+
return cmd.option("-p, --package <name>", "Search specific package (overrides project)").option("-r, --registry <registry>", "Package registry (with --package)", "npm").option("-v, --pkg-version <version>", "Package version (with --package)").option("--project <name>", "Project name (overrides config)").option("-q, --query <query>", "Search query (alternative to argument)").option("-k, --keywords <words>", "Comma-separated keywords", (val) => val.split(",").map((s) => s.trim())).option("-l, --limit <n>", "Max results (default: 25)").option("-A, --after <n>", "Lines of context after match (default: 2)").option("-B, --before <n>", "Lines of context before match (default: 2)").option("-C, --context <n>", "Lines of context before and after match").option("--max-matches <n>", "Max matches per page (default: 5)").option("--mode <mode>", "Match mode: or (default), and").option("--refs-only", "Output only page references (for piping)").option("--count", "Output only match counts per page").option("--no-color", "Disable colored output").option("--json", "Output as JSON");
|
|
2301
|
+
}
|
|
2302
|
+
function registerDocsSearchCommand(program) {
|
|
2303
|
+
const cmd = program.command("search [query]").summary("Search documentation").description(SEARCH_DESCRIPTION);
|
|
2304
|
+
addSearchOptions(cmd).action(async (query, options) => {
|
|
2305
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
2306
|
+
const deps = await createContainer();
|
|
2307
|
+
await docsSearchAction(query, options, deps);
|
|
2308
|
+
});
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
// src/commands/shared-colors.ts
|
|
2312
|
+
var colors2 = {
|
|
2313
|
+
reset: "\x1B[0m",
|
|
2314
|
+
bold: "\x1B[1m",
|
|
2315
|
+
dim: "\x1B[2m",
|
|
2316
|
+
green: "\x1B[32m",
|
|
2317
|
+
yellow: "\x1B[33m",
|
|
2318
|
+
blue: "\x1B[34m",
|
|
2319
|
+
magenta: "\x1B[35m",
|
|
2320
|
+
cyan: "\x1B[36m",
|
|
2321
|
+
red: "\x1B[31m"
|
|
2322
|
+
};
|
|
2323
|
+
function shouldUseColors2(noColor) {
|
|
2324
|
+
if (noColor)
|
|
2325
|
+
return false;
|
|
2326
|
+
if (process.env.NO_COLOR !== undefined)
|
|
2327
|
+
return false;
|
|
2328
|
+
return process.stdout.isTTY ?? false;
|
|
2329
|
+
}
|
|
2330
|
+
function success(text, useColors) {
|
|
2331
|
+
const checkmark = useColors ? `${colors2.green}✓${colors2.reset}` : "✓";
|
|
2332
|
+
return `${checkmark} ${text}`;
|
|
2333
|
+
}
|
|
2334
|
+
function error(text, useColors) {
|
|
2335
|
+
const cross = useColors ? `${colors2.red}✗${colors2.reset}` : "✗";
|
|
2336
|
+
return `${cross} ${text}`;
|
|
2337
|
+
}
|
|
2338
|
+
function highlight(text, useColors) {
|
|
2339
|
+
if (!useColors)
|
|
2340
|
+
return text;
|
|
2341
|
+
return `${colors2.bold}${colors2.cyan}${text}${colors2.reset}`;
|
|
2342
|
+
}
|
|
2343
|
+
function dim(text, useColors) {
|
|
2344
|
+
if (!useColors)
|
|
2345
|
+
return text;
|
|
2346
|
+
return `${colors2.dim}${text}${colors2.reset}`;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// src/commands/mcp-init.ts
|
|
2350
|
+
function getCursorConfigPaths(fs, scope) {
|
|
2351
|
+
if (scope === "project") {
|
|
2352
|
+
const cwd = fs.getCwd();
|
|
2353
|
+
const configPath2 = fs.joinPath(cwd, ".cursor", "mcp.json");
|
|
2354
|
+
const backupPath2 = fs.joinPath(cwd, ".cursor", "mcp.json.bak");
|
|
2355
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2356
|
+
}
|
|
2357
|
+
const platform = process.platform;
|
|
2358
|
+
let configPath;
|
|
2359
|
+
let backupPath;
|
|
2360
|
+
if (platform === "win32") {
|
|
2361
|
+
const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
|
|
2362
|
+
configPath = fs.joinPath(appData, "Cursor", "mcp.json");
|
|
2363
|
+
backupPath = fs.joinPath(appData, "Cursor", "mcp.json.bak");
|
|
2364
|
+
} else {
|
|
2365
|
+
const home = fs.getHomeDir();
|
|
2366
|
+
configPath = fs.joinPath(home, ".cursor", "mcp.json");
|
|
2367
|
+
backupPath = fs.joinPath(home, ".cursor", "mcp.json.bak");
|
|
2368
|
+
}
|
|
2369
|
+
return { configPath, backupPath };
|
|
2370
|
+
}
|
|
2371
|
+
function getCodexConfigPaths(fs, scope) {
|
|
2372
|
+
if (scope === "project") {
|
|
2373
|
+
const cwd = fs.getCwd();
|
|
2374
|
+
const configPath2 = fs.joinPath(cwd, ".codex", "config.toml");
|
|
2375
|
+
const backupPath2 = fs.joinPath(cwd, ".codex", "config.toml.bak");
|
|
2376
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2377
|
+
}
|
|
2378
|
+
const home = fs.getHomeDir();
|
|
2379
|
+
const configPath = fs.joinPath(home, ".codex", "config.toml");
|
|
2380
|
+
const backupPath = fs.joinPath(home, ".codex", "config.toml.bak");
|
|
2381
|
+
return { configPath, backupPath };
|
|
2382
|
+
}
|
|
2383
|
+
function getClaudeCodeConfigPaths(fs, scope) {
|
|
2384
|
+
if (scope === "project") {
|
|
2385
|
+
const cwd = fs.getCwd();
|
|
2386
|
+
const configPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json");
|
|
2387
|
+
const backupPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json.bak");
|
|
2388
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2389
|
+
}
|
|
2390
|
+
const platform = process.platform;
|
|
2391
|
+
let configPath;
|
|
2392
|
+
let backupPath;
|
|
2393
|
+
if (platform === "win32") {
|
|
2394
|
+
const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
|
|
2395
|
+
configPath = fs.joinPath(appData, "Claude Code", "mcp.json");
|
|
2396
|
+
backupPath = fs.joinPath(appData, "Claude Code", "mcp.json.bak");
|
|
2397
|
+
} else {
|
|
2398
|
+
const home = fs.getHomeDir();
|
|
2399
|
+
configPath = fs.joinPath(home, ".claude-code", "mcp.json");
|
|
2400
|
+
backupPath = fs.joinPath(home, ".claude-code", "mcp.json.bak");
|
|
2401
|
+
}
|
|
2402
|
+
return { configPath, backupPath };
|
|
2403
|
+
}
|
|
2404
|
+
async function parseConfigFile(fs, path) {
|
|
2405
|
+
const exists = await fs.exists(path);
|
|
2406
|
+
if (!exists) {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
const content = await fs.readFile(path);
|
|
2411
|
+
const parsed = JSON.parse(content);
|
|
2412
|
+
return parsed;
|
|
2413
|
+
} catch {
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async function writeConfigFile(fs, path, config) {
|
|
2418
|
+
const content = JSON.stringify(config, null, 2);
|
|
2419
|
+
await fs.writeFile(path, content);
|
|
2420
|
+
}
|
|
2421
|
+
async function backupConfigFile(fs, configPath, backupPath) {
|
|
2422
|
+
const exists = await fs.exists(configPath);
|
|
2423
|
+
if (!exists) {
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
const content = await fs.readFile(configPath);
|
|
2427
|
+
await fs.writeFile(backupPath, content);
|
|
2428
|
+
}
|
|
2429
|
+
async function canSafelyEdit(fs, configPath) {
|
|
2430
|
+
const exists = await fs.exists(configPath);
|
|
2431
|
+
if (!exists) {
|
|
2432
|
+
return { safe: true };
|
|
2433
|
+
}
|
|
2434
|
+
const config = await parseConfigFile(fs, configPath);
|
|
2435
|
+
if (config === null) {
|
|
2436
|
+
return {
|
|
2437
|
+
safe: false,
|
|
2438
|
+
reason: "Config file exists but cannot be parsed as valid JSON"
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
if (config.mcpServers?.pkgseer) {
|
|
2442
|
+
return {
|
|
2443
|
+
safe: false,
|
|
2444
|
+
reason: "PkgSeer MCP server is already configured"
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
return { safe: true };
|
|
2448
|
+
}
|
|
2449
|
+
function addPkgseerToConfig(config) {
|
|
2450
|
+
const updated = { ...config };
|
|
2451
|
+
if (!updated.mcpServers) {
|
|
2452
|
+
updated.mcpServers = {};
|
|
2453
|
+
}
|
|
2454
|
+
updated.mcpServers.pkgseer = {
|
|
2455
|
+
command: "npx",
|
|
2456
|
+
args: ["-y", "@pkgseer/cli", "mcp", "start"]
|
|
2457
|
+
};
|
|
2458
|
+
return updated;
|
|
2459
|
+
}
|
|
2460
|
+
function showManualInstructions(tool, scope, configPath, useColors) {
|
|
2461
|
+
console.log(`
|
|
2462
|
+
Manual Setup Instructions`);
|
|
2463
|
+
console.log(`─────────────────────────
|
|
2464
|
+
`);
|
|
2465
|
+
console.log(`Config file: ${highlight(configPath, useColors)}
|
|
2466
|
+
`);
|
|
2467
|
+
console.log(`Add the following to your configuration:
|
|
2468
|
+
`);
|
|
2469
|
+
if (tool === "codex") {
|
|
2470
|
+
const tomlConfig = `[mcp_servers.pkgseer]
|
|
2471
|
+
command = "npx"
|
|
2472
|
+
args = ["-y", "@pkgseer/cli", "mcp", "start"]`;
|
|
2473
|
+
console.log(tomlConfig);
|
|
2474
|
+
} else {
|
|
2475
|
+
const configExample = {
|
|
2476
|
+
mcpServers: {
|
|
2477
|
+
pkgseer: {
|
|
2478
|
+
command: "npx",
|
|
2479
|
+
args: ["-y", "@pkgseer/cli", "mcp", "start"]
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
console.log(JSON.stringify(configExample, null, 2));
|
|
2484
|
+
}
|
|
2485
|
+
if ((tool === "cursor" || tool === "codex" || tool === "claude-code") && scope === "project") {
|
|
2486
|
+
const dirName = tool === "cursor" ? ".cursor" : tool === "codex" ? ".codex" : ".claude-code";
|
|
2487
|
+
console.log(dim(`
|
|
2488
|
+
Note: Create the ${dirName} directory if it doesn't exist.`, useColors));
|
|
2489
|
+
}
|
|
2490
|
+
console.log(dim(`
|
|
2491
|
+
After editing, restart your AI assistant to activate the MCP server.`, useColors));
|
|
2492
|
+
}
|
|
2493
|
+
async function mcpInitAction(deps) {
|
|
2494
|
+
const { fileSystemService: fs, promptService, hasProject } = deps;
|
|
2495
|
+
const useColors = shouldUseColors2();
|
|
2496
|
+
console.log("MCP Server Setup");
|
|
2497
|
+
console.log(`────────────────
|
|
2498
|
+
`);
|
|
2499
|
+
console.log(`Configure PkgSeer as an MCP server for your AI assistant.
|
|
2500
|
+
`);
|
|
2501
|
+
if (!hasProject) {
|
|
2502
|
+
console.log(dim(`Note: No pkgseer.yml found in this directory.
|
|
2503
|
+
`, useColors));
|
|
2504
|
+
console.log(dim(`Without it, search_project_docs won't work (searches your project's packages).
|
|
2505
|
+
`, useColors));
|
|
2506
|
+
const setupProject = await promptService.confirm("Set up project configuration first?", false);
|
|
2507
|
+
if (setupProject) {
|
|
2508
|
+
console.log(`
|
|
2509
|
+
Run ${highlight("pkgseer project init", useColors)} first, then ${highlight("pkgseer mcp init", useColors)} again.
|
|
2510
|
+
`);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const tool = await promptService.select("Which AI tool would you like to configure?", [
|
|
2515
|
+
{
|
|
2516
|
+
value: "cursor",
|
|
2517
|
+
name: "Cursor IDE",
|
|
2518
|
+
description: "Project-level or global configuration"
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
value: "codex",
|
|
2522
|
+
name: "Codex CLI",
|
|
2523
|
+
description: "Project-level or global configuration"
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
value: "claude-code",
|
|
2527
|
+
name: "Claude Code",
|
|
2528
|
+
description: "Project-level or global configuration"
|
|
2529
|
+
},
|
|
2530
|
+
{
|
|
2531
|
+
value: "other",
|
|
2532
|
+
name: "Other",
|
|
2533
|
+
description: "Show manual setup instructions"
|
|
2534
|
+
}
|
|
2535
|
+
]);
|
|
2536
|
+
if (tool === "other") {
|
|
2537
|
+
const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
|
|
2538
|
+
showManualInstructions("other", undefined, configPath2, useColors);
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
let scope;
|
|
2542
|
+
if (tool === "cursor" || tool === "codex" || tool === "claude-code") {
|
|
2543
|
+
const projectPath = tool === "cursor" ? ".cursor/mcp.json" : tool === "codex" ? ".codex/config.toml" : ".claude-code/mcp.json";
|
|
2544
|
+
const globalPath = tool === "cursor" ? "~/.cursor/mcp.json" : tool === "codex" ? "~/.codex/config.toml" : "~/.claude-code/mcp.json";
|
|
2545
|
+
if (hasProject) {
|
|
2546
|
+
scope = await promptService.select("Where should the MCP config be created?", [
|
|
2547
|
+
{
|
|
2548
|
+
value: "project",
|
|
2549
|
+
name: `Project (${projectPath}) – recommended`,
|
|
2550
|
+
description: "Uses project token; enables docs search for your packages"
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
value: "global",
|
|
2554
|
+
name: `Global (${globalPath})`,
|
|
2555
|
+
description: "Works everywhere but without project-specific features"
|
|
2556
|
+
}
|
|
2557
|
+
]);
|
|
2558
|
+
} else {
|
|
2559
|
+
console.log(dim(`
|
|
2560
|
+
Using global config (no pkgseer.yml found).
|
|
2561
|
+
`, useColors));
|
|
2562
|
+
scope = "global";
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
let configPath;
|
|
2566
|
+
let backupPath;
|
|
2567
|
+
if (tool === "cursor") {
|
|
2568
|
+
if (!scope) {
|
|
2569
|
+
scope = "global";
|
|
2570
|
+
}
|
|
2571
|
+
const paths = getCursorConfigPaths(fs, scope);
|
|
2572
|
+
configPath = paths.configPath;
|
|
2573
|
+
backupPath = paths.backupPath;
|
|
2574
|
+
} else if (tool === "codex") {
|
|
2575
|
+
if (!scope) {
|
|
2576
|
+
scope = hasProject ? "project" : "global";
|
|
2577
|
+
}
|
|
2578
|
+
const paths = getCodexConfigPaths(fs, scope);
|
|
2579
|
+
showManualInstructions("codex", scope, paths.configPath, useColors);
|
|
2580
|
+
return;
|
|
2581
|
+
} else if (tool === "claude-code") {
|
|
2582
|
+
if (!scope) {
|
|
2583
|
+
scope = "global";
|
|
2584
|
+
}
|
|
2585
|
+
const paths = getClaudeCodeConfigPaths(fs, scope);
|
|
2586
|
+
configPath = paths.configPath;
|
|
2587
|
+
backupPath = paths.backupPath;
|
|
2588
|
+
} else {
|
|
2589
|
+
const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
|
|
2590
|
+
showManualInstructions("other", undefined, configPath2, useColors);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const safetyCheck = await canSafelyEdit(fs, configPath);
|
|
2594
|
+
if (!safetyCheck.safe) {
|
|
2595
|
+
console.log(error(`Cannot safely edit config file: ${safetyCheck.reason}`, useColors));
|
|
2596
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const configExists = await fs.exists(configPath);
|
|
2600
|
+
if (configExists) {
|
|
2601
|
+
console.log(`
|
|
2602
|
+
Found existing config at: ${highlight(configPath, useColors)}`);
|
|
2603
|
+
const proceed = await promptService.confirm("Add PkgSeer to this configuration?", true);
|
|
2604
|
+
if (!proceed) {
|
|
2605
|
+
console.log(dim(`
|
|
2606
|
+
Setup cancelled.`, useColors));
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
} else {
|
|
2610
|
+
console.log(`
|
|
2611
|
+
Will create config at: ${highlight(configPath, useColors)}`);
|
|
2612
|
+
const proceed = await promptService.confirm("Proceed?", true);
|
|
2613
|
+
if (!proceed) {
|
|
2614
|
+
console.log(dim(`
|
|
2615
|
+
Setup cancelled.`, useColors));
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const existingConfig = await parseConfigFile(fs, configPath);
|
|
2620
|
+
const config = existingConfig ?? {};
|
|
2621
|
+
if (configExists) {
|
|
2622
|
+
try {
|
|
2623
|
+
await backupConfigFile(fs, configPath, backupPath);
|
|
2624
|
+
console.log(dim(`
|
|
2625
|
+
Backup created: ${backupPath}`, useColors));
|
|
2626
|
+
} catch (backupError) {
|
|
2627
|
+
console.log(error(`Warning: Could not create backup: ${backupError instanceof Error ? backupError.message : String(backupError)}`, useColors));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
const updatedConfig = addPkgseerToConfig(config);
|
|
2631
|
+
try {
|
|
2632
|
+
const dirPath = fs.getDirname(configPath);
|
|
2633
|
+
await fs.ensureDir(dirPath);
|
|
2634
|
+
} catch (dirError) {
|
|
2635
|
+
console.log(error(`Failed to create directory: ${dirError instanceof Error ? dirError.message : String(dirError)}`, useColors));
|
|
2636
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
try {
|
|
2640
|
+
await writeConfigFile(fs, configPath, updatedConfig);
|
|
2641
|
+
} catch (writeError) {
|
|
2642
|
+
console.log(error(`Failed to write config file: ${writeError instanceof Error ? writeError.message : String(writeError)}`, useColors));
|
|
2643
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const toolNames = {
|
|
2647
|
+
cursor: "Cursor IDE",
|
|
2648
|
+
codex: "Codex CLI",
|
|
2649
|
+
"claude-code": "Claude Code",
|
|
2650
|
+
other: "MCP"
|
|
2651
|
+
};
|
|
2652
|
+
console.log(success(`PkgSeer MCP server configured for ${toolNames[tool]}!`, useColors));
|
|
2653
|
+
console.log(`
|
|
2654
|
+
Config file: ${highlight(configPath, useColors)}`);
|
|
2655
|
+
if (configExists) {
|
|
2656
|
+
console.log(`Backup saved: ${highlight(backupPath, useColors)}`);
|
|
2657
|
+
}
|
|
2658
|
+
console.log(dim(`
|
|
2659
|
+
Next steps:
|
|
2660
|
+
1. Restart your AI assistant to activate the MCP server
|
|
2661
|
+
2. Test by asking your assistant about packages`, useColors));
|
|
2662
|
+
console.log(`
|
|
2663
|
+
Available MCP tools:`);
|
|
2664
|
+
console.log(" • package_summary - Get package overview");
|
|
2665
|
+
console.log(" • package_vulnerabilities - Check for security issues");
|
|
2666
|
+
console.log(" • package_quality - Get quality score");
|
|
2667
|
+
console.log(" • package_dependencies - List dependencies");
|
|
2668
|
+
console.log(" • compare_packages - Compare multiple packages");
|
|
2669
|
+
console.log(" • list_package_docs - List documentation pages");
|
|
2670
|
+
console.log(" • fetch_package_doc - Fetch documentation content");
|
|
2671
|
+
console.log(" • search_package_docs - Search package documentation");
|
|
2672
|
+
if (hasProject) {
|
|
2673
|
+
console.log(" • search_project_docs - Search your project's docs");
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
var MCP_INIT_DESCRIPTION = `Configure PkgSeer's MCP server for your AI assistant.
|
|
2677
|
+
|
|
2678
|
+
Guides you through:
|
|
2679
|
+
• Selecting your AI tool (Cursor IDE, Codex CLI, Claude Code)
|
|
2680
|
+
• Choosing configuration location (project or global)
|
|
2681
|
+
• Safely editing config files with automatic backup
|
|
2682
|
+
|
|
2683
|
+
Project-level config (recommended):
|
|
2684
|
+
• Uses your project token for authenticated features
|
|
2685
|
+
• Enables search_project_docs (search your project's packages)
|
|
2686
|
+
• Portable with your project`;
|
|
2687
|
+
function registerMcpInitCommand(mcpCommand) {
|
|
2688
|
+
mcpCommand.command("init").summary("Configure MCP server for AI assistants").description(MCP_INIT_DESCRIPTION).action(async () => {
|
|
2689
|
+
const deps = await createContainer();
|
|
2690
|
+
const hasProject = deps.config.project !== undefined;
|
|
2691
|
+
await mcpInitAction({
|
|
2692
|
+
fileSystemService: deps.fileSystemService,
|
|
2693
|
+
promptService: deps.promptService,
|
|
2694
|
+
configService: deps.configService,
|
|
2695
|
+
baseUrl: deps.baseUrl,
|
|
2696
|
+
hasProject
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/services/gitignore-parser.ts
|
|
2702
|
+
function parseGitIgnoreLine(line) {
|
|
2703
|
+
const trimmed = line.trimEnd();
|
|
2704
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2705
|
+
return null;
|
|
2706
|
+
}
|
|
2707
|
+
const isNegation = trimmed.startsWith("!");
|
|
2708
|
+
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
2709
|
+
const isRootOnly = pattern.startsWith("/");
|
|
2710
|
+
const patternWithoutRoot = isRootOnly ? pattern.slice(1) : pattern;
|
|
2711
|
+
const isDirectory = patternWithoutRoot.endsWith("/");
|
|
2712
|
+
const cleanPattern = isDirectory ? patternWithoutRoot.slice(0, -1) : patternWithoutRoot;
|
|
2713
|
+
return {
|
|
2714
|
+
pattern: cleanPattern,
|
|
2715
|
+
isNegation,
|
|
2716
|
+
isDirectory,
|
|
2717
|
+
isRootOnly
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function matchesPattern(path, pattern) {
|
|
2721
|
+
const { pattern: p, isDirectory, isRootOnly } = pattern;
|
|
2722
|
+
let regexStr = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/___DOUBLE_STAR___/g, ".*");
|
|
2723
|
+
if (isRootOnly) {
|
|
2724
|
+
if (isDirectory) {
|
|
2725
|
+
regexStr = `^${regexStr}(/|$)`;
|
|
2726
|
+
} else {
|
|
2727
|
+
regexStr = `^${regexStr}(/|$)`;
|
|
2728
|
+
}
|
|
2729
|
+
} else {
|
|
2730
|
+
if (isDirectory) {
|
|
2731
|
+
regexStr = `(^|/)${regexStr}(/|$)`;
|
|
2732
|
+
} else {
|
|
2733
|
+
regexStr = `(^|/)${regexStr}(/|$)`;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
const regex = new RegExp(regexStr);
|
|
2737
|
+
return regex.test(path);
|
|
2738
|
+
}
|
|
2739
|
+
async function parseGitIgnore(gitignorePath, fs) {
|
|
2740
|
+
try {
|
|
2741
|
+
const content = await fs.readFile(gitignorePath);
|
|
2742
|
+
const lines = content.split(`
|
|
2743
|
+
`);
|
|
2744
|
+
const patterns = [];
|
|
2745
|
+
for (const line of lines) {
|
|
2746
|
+
const pattern = parseGitIgnoreLine(line);
|
|
2747
|
+
if (pattern) {
|
|
2748
|
+
patterns.push(pattern);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
return patterns.length > 0 ? patterns : null;
|
|
2752
|
+
} catch {
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function shouldIgnorePath(relativePath, patterns) {
|
|
2757
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2758
|
+
let ignored = false;
|
|
2759
|
+
for (const pattern of patterns) {
|
|
2760
|
+
if (matchesPattern(normalized, pattern)) {
|
|
2761
|
+
if (pattern.isNegation) {
|
|
2762
|
+
ignored = false;
|
|
2763
|
+
} else {
|
|
2764
|
+
ignored = true;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return ignored;
|
|
2769
|
+
}
|
|
2770
|
+
function shouldIgnoreDirectory(relativeDirPath, patterns) {
|
|
2771
|
+
const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
2772
|
+
return shouldIgnorePath(normalized, patterns);
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/services/manifest-detector.ts
|
|
2776
|
+
var DEFAULT_EXCLUDED_DIRS = [
|
|
2777
|
+
"node_modules",
|
|
2778
|
+
"vendor",
|
|
2779
|
+
".git",
|
|
2780
|
+
"dist",
|
|
2781
|
+
"build",
|
|
2782
|
+
"__pycache__",
|
|
2783
|
+
".venv",
|
|
2784
|
+
"venv",
|
|
2785
|
+
".next"
|
|
2786
|
+
];
|
|
2787
|
+
var MANIFEST_TYPES = [
|
|
2788
|
+
{
|
|
2789
|
+
type: "npm",
|
|
2790
|
+
filenames: [
|
|
2791
|
+
"package.json",
|
|
2792
|
+
"package-lock.json",
|
|
2793
|
+
"yarn.lock",
|
|
2794
|
+
"pnpm-lock.yaml"
|
|
2795
|
+
]
|
|
2796
|
+
},
|
|
2797
|
+
{
|
|
2798
|
+
type: "pypi",
|
|
2799
|
+
filenames: ["requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock"]
|
|
2800
|
+
},
|
|
2801
|
+
{
|
|
2802
|
+
type: "hex",
|
|
2803
|
+
filenames: ["mix.exs", "mix.lock"]
|
|
2804
|
+
}
|
|
2805
|
+
];
|
|
2806
|
+
function suggestLabel(relativePath) {
|
|
2807
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2808
|
+
const parts = normalized.split("/").filter((p) => p.length > 0);
|
|
2809
|
+
if (parts.length === 1) {
|
|
2810
|
+
return "root";
|
|
2811
|
+
}
|
|
2812
|
+
return parts[parts.length - 2];
|
|
2813
|
+
}
|
|
2814
|
+
async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
|
|
2815
|
+
const detected = [];
|
|
2816
|
+
if (currentDepth > options.maxDepth) {
|
|
2817
|
+
return detected;
|
|
2818
|
+
}
|
|
2819
|
+
const relativeDirPath = directory === rootDir ? "." : directory.replace(rootDir, "").replace(/^[/\\]/, "");
|
|
2820
|
+
const baseName = directory.split(/[/\\]/).pop() ?? "";
|
|
2821
|
+
if (options.excludedDirs.includes(baseName)) {
|
|
2822
|
+
return detected;
|
|
2823
|
+
}
|
|
2824
|
+
if (gitignorePatterns && shouldIgnoreDirectory(relativeDirPath, gitignorePatterns)) {
|
|
2825
|
+
return detected;
|
|
2826
|
+
}
|
|
2827
|
+
for (const manifestType of MANIFEST_TYPES) {
|
|
2828
|
+
for (const filename of manifestType.filenames) {
|
|
2829
|
+
const filePath = fs.joinPath(directory, filename);
|
|
2830
|
+
const exists = await fs.exists(filePath);
|
|
2831
|
+
if (exists) {
|
|
2832
|
+
const relativePath = directory === rootDir ? filename : fs.joinPath(directory.replace(rootDir, "").replace(/^[/\\]/, ""), filename);
|
|
2833
|
+
if (gitignorePatterns && shouldIgnorePath(relativePath, gitignorePatterns)) {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
detected.push({
|
|
2837
|
+
filename,
|
|
2838
|
+
relativePath,
|
|
2839
|
+
absolutePath: filePath,
|
|
2840
|
+
type: manifestType.type,
|
|
2841
|
+
suggestedLabel: suggestLabel(relativePath)
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
try {
|
|
2847
|
+
const entries = await fs.readdir(directory);
|
|
2848
|
+
for (const entry of entries) {
|
|
2849
|
+
const entryPath = fs.joinPath(directory, entry);
|
|
2850
|
+
const isDir = await fs.isDirectory(entryPath);
|
|
2851
|
+
if (isDir && !options.excludedDirs.includes(entry)) {
|
|
2852
|
+
const subRelativePath = fs.joinPath(relativeDirPath, entry);
|
|
2853
|
+
if (!gitignorePatterns || !shouldIgnoreDirectory(subRelativePath, gitignorePatterns)) {
|
|
2854
|
+
const subDetected = await scanDirectoryRecursive(entryPath, fs, rootDir, options, currentDepth + 1, gitignorePatterns);
|
|
2855
|
+
detected.push(...subDetected);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
} catch {}
|
|
2860
|
+
return detected;
|
|
2861
|
+
}
|
|
2862
|
+
async function scanForManifests(directory, fs, options) {
|
|
2863
|
+
const opts = {
|
|
2864
|
+
maxDepth: options?.maxDepth ?? 3,
|
|
2865
|
+
excludedDirs: options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS
|
|
2866
|
+
};
|
|
2867
|
+
const gitignorePath = fs.joinPath(directory, ".gitignore");
|
|
2868
|
+
const gitignorePatterns = await parseGitIgnore(gitignorePath, fs);
|
|
2869
|
+
return scanDirectoryRecursive(directory, fs, directory, opts, 0, gitignorePatterns);
|
|
2870
|
+
}
|
|
2871
|
+
function filterRedundantPackageJson(manifests) {
|
|
2872
|
+
const dirToManifests = new Map;
|
|
2873
|
+
for (const manifest of manifests) {
|
|
2874
|
+
const dir = manifest.relativePath.split(/[/\\]/).slice(0, -1).join("/") || ".";
|
|
2875
|
+
if (!dirToManifests.has(dir)) {
|
|
2876
|
+
dirToManifests.set(dir, []);
|
|
2877
|
+
}
|
|
2878
|
+
dirToManifests.get(dir).push(manifest);
|
|
2879
|
+
}
|
|
2880
|
+
const filtered = [];
|
|
2881
|
+
for (const [dir, dirManifests] of dirToManifests.entries()) {
|
|
2882
|
+
const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
|
|
2883
|
+
const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
|
|
2884
|
+
for (const manifest of dirManifests) {
|
|
2885
|
+
if (manifest.filename === "package.json" && hasLockFile) {
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
filtered.push(manifest);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
return filtered;
|
|
2892
|
+
}
|
|
2893
|
+
async function detectAndGroupManifests(directory, fs, options) {
|
|
2894
|
+
const manifests = await scanForManifests(directory, fs, options);
|
|
2895
|
+
const filteredManifests = filterRedundantPackageJson(manifests);
|
|
2896
|
+
const groupsMap = new Map;
|
|
2897
|
+
for (const manifest of filteredManifests) {
|
|
2898
|
+
const existing = groupsMap.get(manifest.suggestedLabel) ?? [];
|
|
2899
|
+
existing.push(manifest);
|
|
2900
|
+
groupsMap.set(manifest.suggestedLabel, existing);
|
|
2901
|
+
}
|
|
2902
|
+
const groups = [];
|
|
2903
|
+
for (const [label, manifests2] of groupsMap.entries()) {
|
|
2904
|
+
const ecosystems = new Set(manifests2.map((m) => m.type));
|
|
2905
|
+
if (ecosystems.size > 1) {
|
|
2906
|
+
const ecosystemGroups = new Map;
|
|
2907
|
+
for (const manifest of manifests2) {
|
|
2908
|
+
const ecosystemLabel = `${label}-${manifest.type}`;
|
|
2909
|
+
const existing = ecosystemGroups.get(ecosystemLabel) ?? [];
|
|
2910
|
+
existing.push(manifest);
|
|
2911
|
+
ecosystemGroups.set(ecosystemLabel, existing);
|
|
2912
|
+
}
|
|
2913
|
+
for (const [
|
|
2914
|
+
ecosystemLabel,
|
|
2915
|
+
ecosystemManifests
|
|
2916
|
+
] of ecosystemGroups.entries()) {
|
|
2917
|
+
groups.push({ label: ecosystemLabel, manifests: ecosystemManifests });
|
|
2918
|
+
}
|
|
2919
|
+
} else {
|
|
2920
|
+
groups.push({ label, manifests: manifests2 });
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
groups.sort((a, b) => {
|
|
2924
|
+
if (a.label.startsWith("root")) {
|
|
2925
|
+
if (b.label.startsWith("root")) {
|
|
2926
|
+
return a.label.localeCompare(b.label);
|
|
2927
|
+
}
|
|
2928
|
+
return -1;
|
|
2929
|
+
}
|
|
2930
|
+
if (b.label.startsWith("root")) {
|
|
2931
|
+
return 1;
|
|
2932
|
+
}
|
|
2933
|
+
return a.label.localeCompare(b.label);
|
|
2934
|
+
});
|
|
2935
|
+
return groups;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/commands/project/manifest-upload-utils.ts
|
|
2939
|
+
async function processManifestFiles(params) {
|
|
2940
|
+
const {
|
|
2941
|
+
files,
|
|
2942
|
+
basePath,
|
|
2943
|
+
hasHexManifests,
|
|
2944
|
+
allowMixDeps,
|
|
2945
|
+
fileSystemService,
|
|
2946
|
+
shellService
|
|
2947
|
+
} = params;
|
|
2948
|
+
const hexFiles = files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
2949
|
+
let generatedHexFiles = [];
|
|
2950
|
+
if (hasHexManifests && allowMixDeps && hexFiles.length > 0) {
|
|
2951
|
+
const firstHexFile = hexFiles[0];
|
|
2952
|
+
if (!firstHexFile) {
|
|
2953
|
+
throw new Error("No hex files found");
|
|
2954
|
+
}
|
|
2955
|
+
const absolutePath = fileSystemService.joinPath(basePath, firstHexFile);
|
|
2956
|
+
const manifestDir = fileSystemService.getDirname(absolutePath);
|
|
2957
|
+
try {
|
|
2958
|
+
const [depsContent, depsTreeContent] = await Promise.all([
|
|
2959
|
+
shellService.execute("mix deps --all", manifestDir),
|
|
2960
|
+
shellService.execute("mix deps.tree", manifestDir)
|
|
2961
|
+
]);
|
|
2962
|
+
generatedHexFiles = [
|
|
2963
|
+
{
|
|
2964
|
+
filename: "deps.txt",
|
|
2965
|
+
path: absolutePath,
|
|
2966
|
+
content: depsContent
|
|
2967
|
+
},
|
|
2968
|
+
{
|
|
2969
|
+
filename: "deps-tree.txt",
|
|
2970
|
+
path: absolutePath,
|
|
2971
|
+
content: depsTreeContent
|
|
2972
|
+
}
|
|
2973
|
+
];
|
|
2974
|
+
} catch (error2) {
|
|
2975
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
2976
|
+
throw new Error(`Failed to generate dependencies for hex manifest: ${firstHexFile}
|
|
2977
|
+
` + ` Error: ${errorMessage}
|
|
2978
|
+
` + ` Make sure 'mix' is installed and the directory contains a valid Elixir project.`);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
const filePromises = files.map(async (relativePath) => {
|
|
2982
|
+
const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
|
|
2983
|
+
if (isHexFile) {
|
|
2984
|
+
if (!allowMixDeps) {
|
|
2985
|
+
return null;
|
|
2986
|
+
}
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
const absolutePath = fileSystemService.joinPath(basePath, relativePath);
|
|
2990
|
+
const exists = await fileSystemService.exists(absolutePath);
|
|
2991
|
+
if (!exists) {
|
|
2992
|
+
throw new Error(`File not found: ${relativePath}
|
|
2993
|
+
Make sure the file exists and the path in pkgseer.yml is correct.`);
|
|
2994
|
+
}
|
|
2995
|
+
const content = await fileSystemService.readFile(absolutePath);
|
|
2996
|
+
const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
|
|
2997
|
+
return {
|
|
2998
|
+
filename,
|
|
2999
|
+
path: absolutePath,
|
|
3000
|
+
content
|
|
3001
|
+
};
|
|
3002
|
+
});
|
|
3003
|
+
const regularFiles = await Promise.all(filePromises);
|
|
3004
|
+
const result = [];
|
|
3005
|
+
let hexFilesInserted = false;
|
|
3006
|
+
for (let i = 0;i < files.length; i++) {
|
|
3007
|
+
const relativePath = files[i];
|
|
3008
|
+
if (!relativePath) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
|
|
3012
|
+
if (isHexFile && !hexFilesInserted && generatedHexFiles.length > 0) {
|
|
3013
|
+
result.push(...generatedHexFiles);
|
|
3014
|
+
hexFilesInserted = true;
|
|
3015
|
+
} else if (!isHexFile) {
|
|
3016
|
+
const regularFile = regularFiles[i];
|
|
3017
|
+
if (regularFile !== undefined) {
|
|
3018
|
+
result.push(regularFile);
|
|
3019
|
+
}
|
|
3020
|
+
} else {
|
|
3021
|
+
result.push(null);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return result;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/commands/project/init.ts
|
|
3028
|
+
async function projectInitAction(options, deps) {
|
|
3029
|
+
const {
|
|
3030
|
+
projectService,
|
|
3031
|
+
configService,
|
|
3032
|
+
fileSystemService,
|
|
3033
|
+
gitService,
|
|
3034
|
+
promptService,
|
|
3035
|
+
shellService,
|
|
3036
|
+
baseUrl
|
|
3037
|
+
} = deps;
|
|
3038
|
+
const useColors = shouldUseColors2();
|
|
3039
|
+
const auth = await checkProjectWriteScope(configService, baseUrl);
|
|
3040
|
+
if (!auth) {
|
|
3041
|
+
console.error(error(`Authentication required with ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope`, useColors));
|
|
3042
|
+
console.log(`
|
|
3043
|
+
Your current token doesn't have the required permissions for creating projects and uploading manifests.`);
|
|
3044
|
+
console.log(`
|
|
3045
|
+
To fix this:`);
|
|
3046
|
+
console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
|
|
3047
|
+
console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
|
|
3048
|
+
console.log(`
|
|
3049
|
+
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
3050
|
+
process.exit(1);
|
|
3051
|
+
}
|
|
3052
|
+
const existingConfig = await configService.loadProjectConfig();
|
|
3053
|
+
if (existingConfig?.config.project) {
|
|
3054
|
+
console.error(error(`A project is already configured in pkgseer.yml: ${highlight(existingConfig.config.project, useColors)}`, useColors));
|
|
3055
|
+
console.log(dim(`
|
|
3056
|
+
To reinitialize, either remove pkgseer.yml or edit it manually.`, useColors));
|
|
3057
|
+
console.log(dim(` To update manifest files, use: `, useColors) + highlight(`pkgseer project detect`, useColors));
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
let projectName = options.name?.trim();
|
|
3061
|
+
if (!projectName) {
|
|
3062
|
+
const cwd2 = fileSystemService.getCwd();
|
|
3063
|
+
const basename = cwd2.split(/[/\\]/).pop() ?? "project";
|
|
3064
|
+
const input2 = await promptService.input(`Project name:`, basename);
|
|
3065
|
+
projectName = input2.trim();
|
|
3066
|
+
}
|
|
3067
|
+
console.log(`
|
|
3068
|
+
Creating project ${highlight(projectName, useColors)}...`);
|
|
3069
|
+
let createResult;
|
|
3070
|
+
let projectAlreadyExists = false;
|
|
3071
|
+
try {
|
|
3072
|
+
createResult = await projectService.createProject({
|
|
3073
|
+
name: projectName
|
|
3074
|
+
});
|
|
3075
|
+
} catch (createError) {
|
|
3076
|
+
const errorMessage = createError instanceof Error ? createError.message : String(createError);
|
|
3077
|
+
if (createError instanceof Error && (errorMessage.includes("already been taken") || errorMessage.includes("already exists"))) {
|
|
3078
|
+
console.log(dim(`
|
|
3079
|
+
Project ${highlight(projectName, useColors)} already exists on the server.`, useColors));
|
|
3080
|
+
console.log(dim(` This might happen if you previously ran init but didn't complete the setup.`, useColors));
|
|
3081
|
+
const useExisting = await promptService.confirm(`
|
|
3082
|
+
Do you want to use the existing project and continue with manifest setup?`, true);
|
|
3083
|
+
if (!useExisting) {
|
|
3084
|
+
console.log(dim(`
|
|
3085
|
+
Exiting. Please choose a different project name or use an existing project.`, useColors));
|
|
3086
|
+
process.exit(0);
|
|
3087
|
+
}
|
|
3088
|
+
projectAlreadyExists = true;
|
|
3089
|
+
createResult = {
|
|
3090
|
+
project: {
|
|
3091
|
+
name: projectName,
|
|
3092
|
+
defaultBranch: "main"
|
|
3093
|
+
},
|
|
3094
|
+
errors: null
|
|
3095
|
+
};
|
|
3096
|
+
} else {
|
|
3097
|
+
console.error(error(`Failed to create project: ${errorMessage}`, useColors));
|
|
3098
|
+
if (createError instanceof Error && (errorMessage.includes("Insufficient permissions") || errorMessage.includes("Required scopes") || errorMessage.includes(PROJECT_MANIFEST_UPLOAD_SCOPE))) {
|
|
3099
|
+
console.log(`
|
|
3100
|
+
Your current token doesn't have the required permissions for creating projects.`);
|
|
3101
|
+
console.log(`
|
|
3102
|
+
To fix this:`);
|
|
3103
|
+
console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
|
|
3104
|
+
console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
|
|
3105
|
+
console.log(`
|
|
3106
|
+
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
3107
|
+
} else if (createError instanceof Error && (errorMessage.includes("alphanumeric") || errorMessage.includes("hyphens") || errorMessage.includes("underscores"))) {
|
|
3108
|
+
console.log(dim(`
|
|
3109
|
+
Project name requirements:`, useColors));
|
|
3110
|
+
console.log(dim(` • Must start with an alphanumeric character (a-z, A-Z, 0-9)`, useColors));
|
|
3111
|
+
console.log(dim(` • Can contain letters, numbers, hyphens (-), and underscores (_)`, useColors));
|
|
3112
|
+
console.log(dim(` • Example valid names: ${highlight(`my-project`, useColors)}, ${highlight(`project_123`, useColors)}, ${highlight(`backend`, useColors)}`, useColors));
|
|
3113
|
+
}
|
|
3114
|
+
process.exit(1);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (!createResult.project) {
|
|
3118
|
+
if (createResult.errors && createResult.errors.length > 0) {
|
|
3119
|
+
const firstError = createResult.errors[0];
|
|
3120
|
+
console.error(error(`Failed to create project: ${firstError?.message ?? "Unknown error"}`, useColors));
|
|
3121
|
+
if (firstError?.field) {
|
|
3122
|
+
console.log(dim(`
|
|
3123
|
+
Field: ${firstError.field}`, useColors));
|
|
3124
|
+
}
|
|
3125
|
+
process.exit(1);
|
|
3126
|
+
}
|
|
3127
|
+
console.error(error(`Failed to create project`, useColors));
|
|
3128
|
+
console.log(dim(`
|
|
3129
|
+
Please try again or contact support if the issue persists.`, useColors));
|
|
3130
|
+
process.exit(1);
|
|
3131
|
+
}
|
|
3132
|
+
if (projectAlreadyExists) {
|
|
3133
|
+
console.log(success(`Using existing project ${highlight(createResult.project.name, useColors)}`, useColors));
|
|
3134
|
+
} else {
|
|
3135
|
+
console.log(success(`Project ${highlight(createResult.project.name, useColors)} created successfully!`, useColors));
|
|
3136
|
+
}
|
|
3137
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
3138
|
+
console.log(dim(`
|
|
3139
|
+
Scanning for manifest files (max depth: ${maxDepth})...`, useColors));
|
|
3140
|
+
const cwd = fileSystemService.getCwd();
|
|
3141
|
+
const manifestGroups = await detectAndGroupManifests(cwd, fileSystemService, {
|
|
3142
|
+
maxDepth
|
|
3143
|
+
});
|
|
3144
|
+
const configToWrite = {
|
|
3145
|
+
project: projectName
|
|
3146
|
+
};
|
|
3147
|
+
if (manifestGroups.length > 0) {
|
|
3148
|
+
console.log(`
|
|
3149
|
+
Found ${highlight(`${manifestGroups.length}`, useColors)} manifest group${manifestGroups.length === 1 ? "" : "s"}:
|
|
3150
|
+
`);
|
|
3151
|
+
for (const group of manifestGroups) {
|
|
3152
|
+
console.log(` Label: ${highlight(group.label, useColors)}`);
|
|
3153
|
+
for (const manifest of group.manifests) {
|
|
3154
|
+
console.log(` ${highlight(manifest.relativePath, useColors)} ${dim(`(${manifest.type})`, useColors)}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
const hasHexManifests = manifestGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
|
|
3158
|
+
let allowMixDeps = false;
|
|
3159
|
+
if (hasHexManifests) {
|
|
3160
|
+
console.log(dim(`
|
|
3161
|
+
Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
|
|
3162
|
+
console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
|
|
3163
|
+
allowMixDeps = await promptService.confirm(`
|
|
3164
|
+
Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
|
|
3165
|
+
}
|
|
3166
|
+
configToWrite.manifests = manifestGroups.map((group) => {
|
|
3167
|
+
const hasHexInGroup = group.manifests.some((m) => m.type === "hex");
|
|
3168
|
+
return {
|
|
3169
|
+
label: group.label,
|
|
3170
|
+
files: group.manifests.map((m) => m.relativePath),
|
|
3171
|
+
...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {}
|
|
3172
|
+
};
|
|
3173
|
+
});
|
|
3174
|
+
const action = await promptService.select(`
|
|
3175
|
+
What would you like to do next?`, [
|
|
3176
|
+
{
|
|
3177
|
+
value: "upload",
|
|
3178
|
+
name: "Save config and upload manifests now",
|
|
3179
|
+
description: "Recommended: saves configuration and uploads files immediately"
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
value: "edit",
|
|
3183
|
+
name: "Save config for manual editing",
|
|
3184
|
+
description: "Saves configuration so you can customize labels before uploading"
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
value: "abort",
|
|
3188
|
+
name: "Skip configuration for now",
|
|
3189
|
+
description: "Project is created but no config saved (you can run init again later)"
|
|
3190
|
+
}
|
|
3191
|
+
]);
|
|
3192
|
+
if (action === "abort") {
|
|
3193
|
+
if (projectAlreadyExists) {
|
|
3194
|
+
console.log(`
|
|
3195
|
+
${success(`Using existing project ${highlight(projectName, useColors)}`, useColors)}`);
|
|
3196
|
+
} else {
|
|
3197
|
+
console.log(`
|
|
3198
|
+
${success(`Project ${highlight(projectName, useColors)} created successfully!`, useColors)}`);
|
|
3199
|
+
}
|
|
3200
|
+
console.log(dim(`
|
|
3201
|
+
Configuration was not saved. To configure later, run:`, useColors));
|
|
3202
|
+
console.log(` ${highlight(`pkgseer project init --name ${projectName}`, useColors)}`);
|
|
3203
|
+
console.log(dim(`
|
|
3204
|
+
Or manually create pkgseer.yml and run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3205
|
+
process.exit(0);
|
|
3206
|
+
}
|
|
3207
|
+
if (action === "upload") {
|
|
3208
|
+
await configService.writeProjectConfig(configToWrite);
|
|
3209
|
+
console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
|
|
3210
|
+
const branch = await gitService.getCurrentBranch() ?? createResult.project.defaultBranch;
|
|
3211
|
+
console.log(`
|
|
3212
|
+
Uploading manifest files to branch ${highlight(branch, useColors)}...`);
|
|
3213
|
+
const cwd2 = fileSystemService.getCwd();
|
|
3214
|
+
for (const group of manifestGroups) {
|
|
3215
|
+
try {
|
|
3216
|
+
const hasHexManifests2 = group.manifests.some((m) => m.type === "hex");
|
|
3217
|
+
const allowMixDeps2 = configToWrite.manifests?.find((m) => m.label === group.label)?.allow_mix_deps === true;
|
|
3218
|
+
const allFiles = await processManifestFiles({
|
|
3219
|
+
files: group.manifests.map((m) => m.relativePath),
|
|
3220
|
+
basePath: cwd2,
|
|
3221
|
+
hasHexManifests: hasHexManifests2,
|
|
3222
|
+
allowMixDeps: allowMixDeps2 ?? false,
|
|
3223
|
+
fileSystemService,
|
|
3224
|
+
shellService
|
|
3225
|
+
});
|
|
3226
|
+
const validFiles = allFiles.filter((f) => f !== null);
|
|
3227
|
+
if (validFiles.length === 0) {
|
|
3228
|
+
console.log(` ${dim(`(no files to upload for ${group.label})`, useColors)}`);
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
const uploadResult = await projectService.uploadManifests({
|
|
3232
|
+
project: projectName,
|
|
3233
|
+
branch,
|
|
3234
|
+
label: group.label,
|
|
3235
|
+
files: validFiles
|
|
3236
|
+
});
|
|
3237
|
+
for (const result of uploadResult.results) {
|
|
3238
|
+
if (result.status === "success") {
|
|
3239
|
+
const depsCount = result.dependencies_count ?? 0;
|
|
3240
|
+
const labelText = highlight(group.label, useColors);
|
|
3241
|
+
const fileText = highlight(result.filename, useColors);
|
|
3242
|
+
const depsText = dim(`(${depsCount} dependencies)`, useColors);
|
|
3243
|
+
console.log(` ${success(`${labelText}: ${fileText}`, useColors)} ${depsText}`);
|
|
3244
|
+
} else {
|
|
3245
|
+
console.log(` ${error(`${group.label}: ${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
} catch (uploadError) {
|
|
3249
|
+
const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
|
|
3250
|
+
let userMessage = `Failed to upload ${group.label}: ${errorMessage}`;
|
|
3251
|
+
if (hasHexManifests) {
|
|
3252
|
+
if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
|
|
3253
|
+
userMessage = `Failed to process hex manifest files for ${group.label}.
|
|
3254
|
+
` + ` Error: ${errorMessage}
|
|
3255
|
+
` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
|
|
3256
|
+
} else if (errorMessage.includes("Failed to generate dependencies")) {
|
|
3257
|
+
userMessage = errorMessage;
|
|
3258
|
+
} else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
|
|
3259
|
+
userMessage = `Failed to upload ${group.label}: Network error.
|
|
3260
|
+
` + ` Error: ${errorMessage}
|
|
3261
|
+
` + ` Check your internet connection and try again.`;
|
|
3262
|
+
}
|
|
3263
|
+
} else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
|
|
3264
|
+
userMessage = `Failed to upload ${group.label}: Network error.
|
|
3265
|
+
` + ` Error: ${errorMessage}
|
|
3266
|
+
` + ` Check your internet connection and try again.`;
|
|
3267
|
+
}
|
|
3268
|
+
console.error(error(userMessage, useColors));
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
console.log(`
|
|
3272
|
+
${success(`Project initialization complete!`, useColors)}`);
|
|
3273
|
+
console.log(dim(`
|
|
3274
|
+
View your project: `, useColors) + highlight(`${deps.baseUrl}/projects/${projectName}`, useColors));
|
|
3275
|
+
console.log(dim(`
|
|
3276
|
+
To upload updated manifests later, run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
} else {
|
|
3280
|
+
console.log(dim(`
|
|
3281
|
+
No manifest files were found in the current directory.`, useColors));
|
|
3282
|
+
}
|
|
3283
|
+
await configService.writeProjectConfig(configToWrite);
|
|
3284
|
+
console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
|
|
3285
|
+
if (manifestGroups.length > 0) {
|
|
3286
|
+
console.log(dim(`
|
|
3287
|
+
Next steps:`, useColors));
|
|
3288
|
+
console.log(dim(` 1. Edit pkgseer.yml to customize manifest labels if needed`, useColors));
|
|
3289
|
+
console.log(dim(` 2. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3290
|
+
console.log(dim(`
|
|
3291
|
+
Tip: Use `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` to automatically update your config when you add new manifest files.`, useColors));
|
|
3292
|
+
} else {
|
|
3293
|
+
console.log(dim(`
|
|
3294
|
+
Next steps:`, useColors));
|
|
3295
|
+
console.log(dim(` 1. Add manifest files to your project`, useColors));
|
|
3296
|
+
console.log(dim(` 2. Edit pkgseer.yml to configure them:`, useColors));
|
|
3297
|
+
console.log(dim(`
|
|
3298
|
+
manifests:`, useColors));
|
|
3299
|
+
console.log(dim(` - label: backend`, useColors));
|
|
3300
|
+
console.log(dim(` files:`, useColors));
|
|
3301
|
+
console.log(dim(` - `, useColors) + highlight(`package-lock.json`, useColors));
|
|
3302
|
+
console.log(dim(`
|
|
3303
|
+
3. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3304
|
+
console.log(dim(`
|
|
3305
|
+
Tip: Run `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` after adding manifest files to auto-configure them.`, useColors));
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
var INIT_DESCRIPTION = `Initialize a new project in the current directory.
|
|
3309
|
+
|
|
3310
|
+
This command will:
|
|
3311
|
+
1. Create a new project entry (or prompt for a project name)
|
|
3312
|
+
2. Scan for manifest files (package.json, requirements.txt, etc.)
|
|
3313
|
+
3. Suggest labels based on directory structure
|
|
3314
|
+
4. Optionally upload manifests to the project
|
|
3315
|
+
|
|
3316
|
+
The project name defaults to the current directory name, or you can
|
|
3317
|
+
specify it with --name. Manifest files are automatically detected
|
|
3318
|
+
and grouped by their directory structure.`;
|
|
3319
|
+
function registerProjectInitCommand(program) {
|
|
3320
|
+
program.command("init").summary("Initialize a new project").description(INIT_DESCRIPTION).option("--name <name>", "Project name (alphanumeric, hyphens, underscores only)").option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).action(async (options) => {
|
|
3321
|
+
const deps = await createContainer();
|
|
3322
|
+
await projectInitAction({ name: options.name, maxDepth: options.maxDepth }, {
|
|
3323
|
+
projectService: deps.projectService,
|
|
3324
|
+
configService: deps.configService,
|
|
3325
|
+
fileSystemService: deps.fileSystemService,
|
|
3326
|
+
gitService: deps.gitService,
|
|
3327
|
+
promptService: deps.promptService,
|
|
3328
|
+
shellService: deps.shellService,
|
|
3329
|
+
baseUrl: deps.baseUrl
|
|
3330
|
+
});
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/commands/init.ts
|
|
3335
|
+
async function initAction(options, deps) {
|
|
3336
|
+
const useColors = shouldUseColors2();
|
|
3337
|
+
console.log("Welcome to PkgSeer!");
|
|
3338
|
+
console.log(`───────────────────
|
|
3339
|
+
`);
|
|
3340
|
+
console.log(`Set up PkgSeer for your project:
|
|
3341
|
+
` + ` 1. Project configuration – track dependencies and monitor vulnerabilities
|
|
3342
|
+
` + ` 2. MCP server – enable AI assistant integration
|
|
3343
|
+
`);
|
|
3344
|
+
console.log(dim(`Or run separately: pkgseer project init, pkgseer mcp init
|
|
3345
|
+
`, useColors));
|
|
3346
|
+
let setupProject = !options.skipProject;
|
|
3347
|
+
let setupMcp = !options.skipMcp;
|
|
3348
|
+
if (options.skipProject && options.skipMcp) {
|
|
3349
|
+
showCliUsage(useColors);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (!options.skipProject && !options.skipMcp) {
|
|
3353
|
+
const choice = await deps.promptService.select("What would you like to set up?", [
|
|
3354
|
+
{
|
|
3355
|
+
value: "both",
|
|
3356
|
+
name: "Both project and MCP server (recommended)",
|
|
3357
|
+
description: "Full setup: dependency tracking + AI assistant integration"
|
|
3358
|
+
},
|
|
3359
|
+
{
|
|
3360
|
+
value: "project",
|
|
3361
|
+
name: "Project only",
|
|
3362
|
+
description: "Track dependencies and monitor for vulnerabilities"
|
|
3363
|
+
},
|
|
3364
|
+
{
|
|
3365
|
+
value: "mcp",
|
|
3366
|
+
name: "MCP server only",
|
|
3367
|
+
description: "Enable AI assistant integration"
|
|
3368
|
+
},
|
|
3369
|
+
{
|
|
3370
|
+
value: "cli",
|
|
3371
|
+
name: "CLI usage (no setup)",
|
|
3372
|
+
description: "Use PkgSeer as a command-line tool"
|
|
3373
|
+
}
|
|
3374
|
+
]);
|
|
3375
|
+
if (choice === "cli") {
|
|
3376
|
+
showCliUsage(useColors);
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
setupProject = choice === "both" || choice === "project";
|
|
3380
|
+
setupMcp = choice === "both" || choice === "mcp";
|
|
3381
|
+
}
|
|
3382
|
+
const projectInit = deps.projectInitAction ?? projectInitAction;
|
|
3383
|
+
const mcpInit = deps.mcpInitAction ?? mcpInitAction;
|
|
3384
|
+
if (setupProject) {
|
|
3385
|
+
const existingConfig = await deps.configService.loadProjectConfig();
|
|
3386
|
+
if (existingConfig?.config.project) {
|
|
3387
|
+
console.log(`
|
|
3388
|
+
Project already configured: ${highlight(existingConfig.config.project, useColors)}`);
|
|
3389
|
+
console.log(dim(`Skipping project setup. Run 'pkgseer project init' to reinitialize.
|
|
3390
|
+
`, useColors));
|
|
3391
|
+
setupProject = false;
|
|
3392
|
+
} else {
|
|
3393
|
+
console.log(`
|
|
3394
|
+
` + "=".repeat(50));
|
|
3395
|
+
console.log("Project Configuration Setup");
|
|
3396
|
+
console.log("=".repeat(50) + `
|
|
3397
|
+
`);
|
|
3398
|
+
await projectInit({}, {
|
|
3399
|
+
projectService: deps.projectService,
|
|
3400
|
+
configService: deps.configService,
|
|
3401
|
+
fileSystemService: deps.fileSystemService,
|
|
3402
|
+
gitService: deps.gitService,
|
|
3403
|
+
promptService: deps.promptService,
|
|
3404
|
+
shellService: deps.shellService,
|
|
3405
|
+
baseUrl: deps.baseUrl
|
|
3406
|
+
});
|
|
3407
|
+
const updatedConfig = await deps.configService.loadProjectConfig();
|
|
3408
|
+
if (updatedConfig?.config.project) {
|
|
3409
|
+
console.log(`
|
|
3410
|
+
${highlight("✓", useColors)} Project setup complete!
|
|
3411
|
+
`);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (setupMcp) {
|
|
3416
|
+
const currentConfig = await deps.configService.loadProjectConfig();
|
|
3417
|
+
const hasProjectNow = currentConfig?.config.project !== undefined;
|
|
3418
|
+
console.log(`
|
|
3419
|
+
` + "=".repeat(50));
|
|
3420
|
+
console.log("MCP Server Setup");
|
|
3421
|
+
console.log("=".repeat(50) + `
|
|
3422
|
+
`);
|
|
3423
|
+
await mcpInit({
|
|
3424
|
+
fileSystemService: deps.fileSystemService,
|
|
3425
|
+
promptService: deps.promptService,
|
|
3426
|
+
configService: deps.configService,
|
|
3427
|
+
baseUrl: deps.baseUrl,
|
|
3428
|
+
hasProject: hasProjectNow
|
|
3429
|
+
});
|
|
3430
|
+
console.log(`
|
|
3431
|
+
${highlight("✓", useColors)} MCP setup complete!
|
|
3432
|
+
`);
|
|
3433
|
+
}
|
|
3434
|
+
console.log("=".repeat(50));
|
|
3435
|
+
console.log("Setup Complete!");
|
|
3436
|
+
console.log("=".repeat(50) + `
|
|
3437
|
+
`);
|
|
3438
|
+
if (setupProject) {
|
|
3439
|
+
const finalConfig = await deps.configService.loadProjectConfig();
|
|
3440
|
+
if (finalConfig?.config.project) {
|
|
3441
|
+
console.log(`Project: ${highlight(finalConfig.config.project, useColors)}`);
|
|
3442
|
+
console.log(dim(` View at: ${deps.baseUrl}/projects/${finalConfig.config.project}`, useColors));
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
if (setupMcp) {
|
|
3446
|
+
console.log("MCP Server: Configured");
|
|
3447
|
+
console.log(dim(" Restart your AI assistant to activate the MCP server.", useColors));
|
|
3448
|
+
}
|
|
3449
|
+
console.log(dim(`
|
|
3450
|
+
Next steps:
|
|
3451
|
+
` + ` • Use CLI commands: pkgseer pkg info <package>
|
|
3452
|
+
` + ` • Search docs: pkgseer docs search <query>
|
|
3453
|
+
` + " • Quick reference: pkgseer quickstart", useColors));
|
|
3454
|
+
}
|
|
3455
|
+
function showCliUsage(useColors) {
|
|
3456
|
+
console.log("Using PkgSeer via CLI");
|
|
3457
|
+
console.log(`─────────────────────
|
|
3458
|
+
`);
|
|
3459
|
+
console.log(`PkgSeer works great as a standalone CLI tool. Your AI assistant
|
|
3460
|
+
` + `can run these commands directly:
|
|
3461
|
+
`);
|
|
3462
|
+
console.log("Package Commands:");
|
|
3463
|
+
console.log(` ${highlight("pkgseer pkg info <package>", useColors)} Get package summary`);
|
|
3464
|
+
console.log(` ${highlight("pkgseer pkg vulns <package>", useColors)} Check vulnerabilities`);
|
|
3465
|
+
console.log(` ${highlight("pkgseer pkg quality <package>", useColors)} Get quality score`);
|
|
3466
|
+
console.log(` ${highlight("pkgseer pkg deps <package>", useColors)} List dependencies`);
|
|
3467
|
+
console.log(` ${highlight("pkgseer pkg compare <pkg...>", useColors)} Compare packages
|
|
3468
|
+
`);
|
|
3469
|
+
console.log("Documentation Commands:");
|
|
3470
|
+
console.log(` ${highlight("pkgseer docs list <package>", useColors)} List doc pages`);
|
|
3471
|
+
console.log(` ${highlight("pkgseer docs get <pkg>/<page>", useColors)} Fetch doc content`);
|
|
3472
|
+
console.log(` ${highlight("pkgseer docs search <query>", useColors)} Search documentation
|
|
3473
|
+
`);
|
|
3474
|
+
console.log(dim(`All commands support --json for structured output.
|
|
3475
|
+
` + "Tip: Run 'pkgseer quickstart' for a quick reference guide.", useColors));
|
|
3476
|
+
}
|
|
3477
|
+
function registerInitCommand(program) {
|
|
3478
|
+
program.command("init").summary("Set up project and MCP server").description(`Set up PkgSeer for your project.
|
|
3479
|
+
|
|
3480
|
+
Guides you through:
|
|
3481
|
+
• Project configuration – track dependencies, monitor vulnerabilities
|
|
3482
|
+
• MCP server – enable AI assistant integration
|
|
3483
|
+
|
|
3484
|
+
Run separately: pkgseer project init, pkgseer mcp init`).option("--skip-project", "Skip project setup").option("--skip-mcp", "Skip MCP setup").action(async (options) => {
|
|
3485
|
+
const deps = await createContainer();
|
|
3486
|
+
await initAction(options, deps);
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/commands/login.ts
|
|
3491
|
+
import { hostname } from "node:os";
|
|
3492
|
+
var TIMEOUT_MS = 5 * 60 * 1000;
|
|
3493
|
+
function randomPort() {
|
|
3494
|
+
return Math.floor(Math.random() * 2000) + 8000;
|
|
3495
|
+
}
|
|
3496
|
+
async function loginAction(options, deps) {
|
|
3497
|
+
const { authService, authStorage, browserService, baseUrl } = deps;
|
|
3498
|
+
const existing = await authStorage.load(baseUrl);
|
|
3499
|
+
if (existing && !options.force) {
|
|
3500
|
+
const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
|
|
3501
|
+
if (!isExpired) {
|
|
3502
|
+
console.log(`Already logged in.
|
|
3503
|
+
`);
|
|
3504
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3505
|
+
console.log(` Token: ${existing.tokenName}
|
|
3506
|
+
`);
|
|
3507
|
+
console.log("To switch accounts, run `pkgseer logout` first.");
|
|
3508
|
+
console.log("To re-authenticate with different scopes, use `pkgseer login --force`.");
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
console.log(`Token expired. Starting new login...
|
|
3512
|
+
`);
|
|
3513
|
+
} else if (existing && options.force) {
|
|
3514
|
+
console.log(`Re-authenticating (--force flag)...
|
|
3515
|
+
`);
|
|
3516
|
+
}
|
|
3517
|
+
const { verifier, challenge, state } = authService.generatePkceParams();
|
|
3518
|
+
const port = options.port ?? randomPort();
|
|
3519
|
+
const authUrl = authService.buildAuthUrl({
|
|
3520
|
+
state,
|
|
3521
|
+
port,
|
|
3522
|
+
codeChallenge: challenge,
|
|
3523
|
+
hostname: hostname()
|
|
3524
|
+
});
|
|
3525
|
+
const serverPromise = authService.startCallbackServer(port);
|
|
3526
|
+
if (options.browser === false) {
|
|
3527
|
+
console.log(`Open this URL in your browser:
|
|
3528
|
+
`);
|
|
3529
|
+
console.log(` ${authUrl}
|
|
3530
|
+
`);
|
|
3531
|
+
} else {
|
|
3532
|
+
console.log("Opening browser...");
|
|
3533
|
+
await browserService.open(authUrl);
|
|
3534
|
+
}
|
|
3535
|
+
console.log(`Waiting for authentication...
|
|
3536
|
+
`);
|
|
3537
|
+
let timeoutId;
|
|
3538
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3539
|
+
timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
|
|
3540
|
+
});
|
|
3541
|
+
let callback;
|
|
3542
|
+
try {
|
|
3543
|
+
callback = await Promise.race([serverPromise, timeoutPromise]);
|
|
3544
|
+
clearTimeout(timeoutId);
|
|
3545
|
+
} catch (error2) {
|
|
3546
|
+
clearTimeout(timeoutId);
|
|
3547
|
+
if (error2 instanceof Error) {
|
|
3548
|
+
console.log(`${error2.message}.
|
|
3549
|
+
`);
|
|
3550
|
+
console.log("Run `pkgseer login` to try again.");
|
|
3551
|
+
}
|
|
3552
|
+
process.exit(1);
|
|
3553
|
+
}
|
|
3554
|
+
if (callback.state !== state) {
|
|
3555
|
+
console.error(`Security error: authentication state mismatch.
|
|
3556
|
+
`);
|
|
3557
|
+
console.log("This could indicate a security issue. Please try again.");
|
|
3558
|
+
process.exit(1);
|
|
3559
|
+
}
|
|
3560
|
+
let tokenResponse;
|
|
3561
|
+
try {
|
|
3562
|
+
tokenResponse = await authService.exchangeCodeForToken({
|
|
3563
|
+
code: callback.code,
|
|
3564
|
+
codeVerifier: verifier,
|
|
3565
|
+
state
|
|
3566
|
+
});
|
|
3567
|
+
} catch (error2) {
|
|
3568
|
+
console.error(`Failed to complete authentication: ${error2 instanceof Error ? error2.message : error2}
|
|
3569
|
+
`);
|
|
3570
|
+
console.log("Run `pkgseer login` to try again.");
|
|
3571
|
+
process.exit(1);
|
|
3572
|
+
}
|
|
3573
|
+
await authStorage.save(baseUrl, {
|
|
3574
|
+
token: tokenResponse.token,
|
|
3575
|
+
tokenName: tokenResponse.tokenName,
|
|
3576
|
+
scopes: tokenResponse.scopes,
|
|
3577
|
+
createdAt: new Date().toISOString(),
|
|
3578
|
+
expiresAt: tokenResponse.expiresAt,
|
|
3579
|
+
apiKeyId: tokenResponse.apiKeyId
|
|
3580
|
+
});
|
|
3581
|
+
console.log(`✓ Logged in
|
|
3582
|
+
`);
|
|
3583
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3584
|
+
console.log(` Token: ${tokenResponse.tokenName}`);
|
|
3585
|
+
if (tokenResponse.expiresAt) {
|
|
3586
|
+
const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
3587
|
+
console.log(` Expires: in ${days} days`);
|
|
3588
|
+
}
|
|
3589
|
+
console.log(`
|
|
3590
|
+
You're ready to use pkgseer with your AI assistant.`);
|
|
3591
|
+
}
|
|
3592
|
+
var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
|
|
3593
|
+
|
|
3594
|
+
Opens your browser to complete authentication securely. The CLI receives
|
|
3595
|
+
a token that's stored locally and used for API requests.
|
|
3596
|
+
|
|
3597
|
+
Use --no-browser in environments without a display (CI, SSH sessions)
|
|
3598
|
+
to get a URL you can open on another device.`;
|
|
3599
|
+
function registerLoginCommand(program) {
|
|
3600
|
+
program.command("login").summary("Authenticate with your PkgSeer account").description(LOGIN_DESCRIPTION).option("--no-browser", "Print URL instead of opening browser").option("--port <port>", "Port for local callback server", parseInt).option("--force", "Re-authenticate even if already logged in").action(async (options) => {
|
|
3601
|
+
const deps = await createContainer();
|
|
3602
|
+
await loginAction(options, deps);
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
// src/commands/logout.ts
|
|
3607
|
+
async function logoutAction(deps) {
|
|
3608
|
+
const { authService, authStorage, baseUrl } = deps;
|
|
3609
|
+
const auth = await authStorage.load(baseUrl);
|
|
3610
|
+
if (!auth) {
|
|
3611
|
+
console.log(`Not currently logged in.
|
|
3612
|
+
`);
|
|
3613
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
try {
|
|
3617
|
+
await authService.revokeToken(auth.token);
|
|
3618
|
+
} catch {}
|
|
3619
|
+
await authStorage.clear(baseUrl);
|
|
3620
|
+
console.log(`✓ Logged out
|
|
3621
|
+
`);
|
|
3622
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3623
|
+
}
|
|
3624
|
+
var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
|
|
3625
|
+
|
|
3626
|
+
Clears the locally stored authentication token and notifies the server
|
|
3627
|
+
to revoke it. Use this when switching accounts or on shared machines.`;
|
|
3628
|
+
function registerLogoutCommand(program) {
|
|
3629
|
+
program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
|
|
3630
|
+
const deps = await createContainer();
|
|
3631
|
+
await logoutAction(deps);
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
// src/commands/mcp.ts
|
|
3636
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
809
3637
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
810
3638
|
|
|
3639
|
+
// src/tools/compare-packages.ts
|
|
3640
|
+
import { z as z3 } from "zod";
|
|
3641
|
+
|
|
3642
|
+
// src/tools/shared.ts
|
|
3643
|
+
import { z as z2 } from "zod";
|
|
3644
|
+
|
|
811
3645
|
// src/tools/types.ts
|
|
812
3646
|
function textResult(text) {
|
|
813
3647
|
return {
|
|
@@ -820,9 +3654,9 @@ function errorResult(message) {
|
|
|
820
3654
|
isError: true
|
|
821
3655
|
};
|
|
822
3656
|
}
|
|
3657
|
+
|
|
823
3658
|
// src/tools/shared.ts
|
|
824
|
-
|
|
825
|
-
function toGraphQLRegistry(registry) {
|
|
3659
|
+
function toGraphQLRegistry2(registry) {
|
|
826
3660
|
const map = {
|
|
827
3661
|
npm: "NPM",
|
|
828
3662
|
pypi: "PYPI",
|
|
@@ -831,9 +3665,9 @@ function toGraphQLRegistry(registry) {
|
|
|
831
3665
|
return map[registry.toLowerCase()] || "NPM";
|
|
832
3666
|
}
|
|
833
3667
|
var schemas = {
|
|
834
|
-
registry:
|
|
835
|
-
packageName:
|
|
836
|
-
version:
|
|
3668
|
+
registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
|
|
3669
|
+
packageName: z2.string().max(255).describe("Name of the package"),
|
|
3670
|
+
version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
|
|
837
3671
|
};
|
|
838
3672
|
function handleGraphQLErrors(errors) {
|
|
839
3673
|
if (errors && errors.length > 0) {
|
|
@@ -844,80 +3678,117 @@ function handleGraphQLErrors(errors) {
|
|
|
844
3678
|
async function withErrorHandling(operation, fn) {
|
|
845
3679
|
try {
|
|
846
3680
|
return await fn();
|
|
847
|
-
} catch (
|
|
848
|
-
const message =
|
|
3681
|
+
} catch (error2) {
|
|
3682
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
849
3683
|
return errorResult(`Failed to ${operation}: ${message}`);
|
|
850
3684
|
}
|
|
851
3685
|
}
|
|
852
3686
|
function notFoundError(packageName, registry) {
|
|
853
3687
|
return errorResult(`Package not found: ${packageName} in ${registry}`);
|
|
854
3688
|
}
|
|
855
|
-
|
|
3689
|
+
|
|
3690
|
+
// src/tools/compare-packages.ts
|
|
3691
|
+
var packageInputSchema = z3.object({
|
|
3692
|
+
registry: z3.enum(["npm", "pypi", "hex"]),
|
|
3693
|
+
name: z3.string().max(255),
|
|
3694
|
+
version: z3.string().max(100).optional()
|
|
3695
|
+
});
|
|
856
3696
|
var argsSchema = {
|
|
857
|
-
|
|
858
|
-
package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
|
|
3697
|
+
packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
|
|
859
3698
|
};
|
|
860
|
-
function
|
|
3699
|
+
function createComparePackagesTool(pkgseerService) {
|
|
861
3700
|
return {
|
|
862
|
-
name: "
|
|
863
|
-
description: "
|
|
3701
|
+
name: "compare_packages",
|
|
3702
|
+
description: "Compares multiple packages across metadata, quality, and security dimensions",
|
|
864
3703
|
schema: argsSchema,
|
|
865
|
-
handler: async ({
|
|
866
|
-
return withErrorHandling("
|
|
867
|
-
const
|
|
3704
|
+
handler: async ({ packages }, _extra) => {
|
|
3705
|
+
return withErrorHandling("compare packages", async () => {
|
|
3706
|
+
const input2 = packages.map((pkg) => ({
|
|
3707
|
+
registry: toGraphQLRegistry2(pkg.registry),
|
|
3708
|
+
name: pkg.name,
|
|
3709
|
+
version: pkg.version
|
|
3710
|
+
}));
|
|
3711
|
+
const result = await pkgseerService.comparePackages(input2);
|
|
868
3712
|
const graphqlError = handleGraphQLErrors(result.errors);
|
|
869
3713
|
if (graphqlError)
|
|
870
3714
|
return graphqlError;
|
|
871
|
-
if (!result.data.
|
|
872
|
-
return
|
|
3715
|
+
if (!result.data.comparePackages) {
|
|
3716
|
+
return errorResult("Comparison failed: no results returned");
|
|
873
3717
|
}
|
|
874
|
-
return textResult(JSON.stringify(result.data.
|
|
3718
|
+
return textResult(JSON.stringify(result.data.comparePackages, null, 2));
|
|
875
3719
|
});
|
|
876
3720
|
}
|
|
877
3721
|
};
|
|
878
3722
|
}
|
|
879
|
-
// src/tools/package-
|
|
3723
|
+
// src/tools/fetch-package-doc.ts
|
|
3724
|
+
import { z as z4 } from "zod";
|
|
880
3725
|
var argsSchema2 = {
|
|
881
3726
|
registry: schemas.registry,
|
|
882
|
-
package_name: schemas.packageName.describe("Name of the package to
|
|
3727
|
+
package_name: schemas.packageName.describe("Name of the package to fetch documentation for"),
|
|
3728
|
+
page_id: z4.string().max(500).describe("Documentation page identifier (from list_package_docs)"),
|
|
883
3729
|
version: schemas.version
|
|
884
3730
|
};
|
|
885
|
-
function
|
|
3731
|
+
function createFetchPackageDocTool(pkgseerService) {
|
|
886
3732
|
return {
|
|
887
|
-
name: "
|
|
888
|
-
description: "
|
|
3733
|
+
name: "fetch_package_doc",
|
|
3734
|
+
description: "Fetches the full content of a specific documentation page. Returns page title, content (markdown/HTML), breadcrumbs, and source attribution. Use list_package_docs first to get available page IDs.",
|
|
889
3735
|
schema: argsSchema2,
|
|
3736
|
+
handler: async ({ registry, package_name, page_id, version: version2 }, _extra) => {
|
|
3737
|
+
return withErrorHandling("fetch documentation page", async () => {
|
|
3738
|
+
const result = await pkgseerService.fetchPackageDoc(toGraphQLRegistry2(registry), package_name, page_id, version2);
|
|
3739
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3740
|
+
if (graphqlError)
|
|
3741
|
+
return graphqlError;
|
|
3742
|
+
if (!result.data.fetchPackageDoc) {
|
|
3743
|
+
return errorResult(`Documentation page not found: ${page_id} for ${package_name} in ${registry}`);
|
|
3744
|
+
}
|
|
3745
|
+
return textResult(JSON.stringify(result.data.fetchPackageDoc, null, 2));
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
// src/tools/list-package-docs.ts
|
|
3751
|
+
var argsSchema3 = {
|
|
3752
|
+
registry: schemas.registry,
|
|
3753
|
+
package_name: schemas.packageName.describe("Name of the package to list documentation for"),
|
|
3754
|
+
version: schemas.version
|
|
3755
|
+
};
|
|
3756
|
+
function createListPackageDocsTool(pkgseerService) {
|
|
3757
|
+
return {
|
|
3758
|
+
name: "list_package_docs",
|
|
3759
|
+
description: "Lists documentation pages for a package version. Returns page titles, slugs, and metadata. Use this to discover available documentation before fetching specific pages.",
|
|
3760
|
+
schema: argsSchema3,
|
|
890
3761
|
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
891
|
-
return withErrorHandling("
|
|
892
|
-
const result = await pkgseerService.
|
|
3762
|
+
return withErrorHandling("list package documentation", async () => {
|
|
3763
|
+
const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
|
|
893
3764
|
const graphqlError = handleGraphQLErrors(result.errors);
|
|
894
3765
|
if (graphqlError)
|
|
895
3766
|
return graphqlError;
|
|
896
|
-
if (!result.data.
|
|
3767
|
+
if (!result.data.listPackageDocs) {
|
|
897
3768
|
return notFoundError(package_name, registry);
|
|
898
3769
|
}
|
|
899
|
-
return textResult(JSON.stringify(result.data.
|
|
3770
|
+
return textResult(JSON.stringify(result.data.listPackageDocs, null, 2));
|
|
900
3771
|
});
|
|
901
3772
|
}
|
|
902
3773
|
};
|
|
903
3774
|
}
|
|
904
3775
|
// src/tools/package-dependencies.ts
|
|
905
|
-
import { z as
|
|
906
|
-
var
|
|
3776
|
+
import { z as z5 } from "zod";
|
|
3777
|
+
var argsSchema4 = {
|
|
907
3778
|
registry: schemas.registry,
|
|
908
3779
|
package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
|
|
909
3780
|
version: schemas.version,
|
|
910
|
-
include_transitive:
|
|
911
|
-
max_depth:
|
|
3781
|
+
include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
|
|
3782
|
+
max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
|
|
912
3783
|
};
|
|
913
3784
|
function createPackageDependenciesTool(pkgseerService) {
|
|
914
3785
|
return {
|
|
915
3786
|
name: "package_dependencies",
|
|
916
3787
|
description: "Retrieves direct and transitive dependencies for a package version",
|
|
917
|
-
schema:
|
|
3788
|
+
schema: argsSchema4,
|
|
918
3789
|
handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
|
|
919
3790
|
return withErrorHandling("fetch package dependencies", async () => {
|
|
920
|
-
const result = await pkgseerService.getPackageDependencies(
|
|
3791
|
+
const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
|
|
921
3792
|
const graphqlError = handleGraphQLErrors(result.errors);
|
|
922
3793
|
if (graphqlError)
|
|
923
3794
|
return graphqlError;
|
|
@@ -930,7 +3801,7 @@ function createPackageDependenciesTool(pkgseerService) {
|
|
|
930
3801
|
};
|
|
931
3802
|
}
|
|
932
3803
|
// src/tools/package-quality.ts
|
|
933
|
-
var
|
|
3804
|
+
var argsSchema5 = {
|
|
934
3805
|
registry: schemas.registry,
|
|
935
3806
|
package_name: schemas.packageName.describe("Name of the package to analyze"),
|
|
936
3807
|
version: schemas.version
|
|
@@ -939,10 +3810,10 @@ function createPackageQualityTool(pkgseerService) {
|
|
|
939
3810
|
return {
|
|
940
3811
|
name: "package_quality",
|
|
941
3812
|
description: "Retrieves quality score and rule-level breakdown for a package",
|
|
942
|
-
schema:
|
|
3813
|
+
schema: argsSchema5,
|
|
943
3814
|
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
944
3815
|
return withErrorHandling("fetch package quality", async () => {
|
|
945
|
-
const result = await pkgseerService.getPackageQuality(
|
|
3816
|
+
const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
|
|
946
3817
|
const graphqlError = handleGraphQLErrors(result.errors);
|
|
947
3818
|
if (graphqlError)
|
|
948
3819
|
return graphqlError;
|
|
@@ -954,55 +3825,178 @@ function createPackageQualityTool(pkgseerService) {
|
|
|
954
3825
|
}
|
|
955
3826
|
};
|
|
956
3827
|
}
|
|
957
|
-
// src/tools/
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
name: z3.string().max(255),
|
|
962
|
-
version: z3.string().max(100).optional()
|
|
963
|
-
});
|
|
964
|
-
var argsSchema5 = {
|
|
965
|
-
packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
|
|
3828
|
+
// src/tools/package-summary.ts
|
|
3829
|
+
var argsSchema6 = {
|
|
3830
|
+
registry: schemas.registry,
|
|
3831
|
+
package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
|
|
966
3832
|
};
|
|
967
|
-
function
|
|
3833
|
+
function createPackageSummaryTool(pkgseerService) {
|
|
968
3834
|
return {
|
|
969
|
-
name: "
|
|
970
|
-
description: "
|
|
971
|
-
schema:
|
|
972
|
-
handler: async ({
|
|
973
|
-
return withErrorHandling("
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
3835
|
+
name: "package_summary",
|
|
3836
|
+
description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
|
|
3837
|
+
schema: argsSchema6,
|
|
3838
|
+
handler: async ({ registry, package_name }, _extra) => {
|
|
3839
|
+
return withErrorHandling("fetch package summary", async () => {
|
|
3840
|
+
const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
|
|
3841
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3842
|
+
if (graphqlError)
|
|
3843
|
+
return graphqlError;
|
|
3844
|
+
if (!result.data.packageSummary) {
|
|
3845
|
+
return notFoundError(package_name, registry);
|
|
3846
|
+
}
|
|
3847
|
+
return textResult(JSON.stringify(result.data.packageSummary, null, 2));
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
// src/tools/package-vulnerabilities.ts
|
|
3853
|
+
var argsSchema7 = {
|
|
3854
|
+
registry: schemas.registry,
|
|
3855
|
+
package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
|
|
3856
|
+
version: schemas.version
|
|
3857
|
+
};
|
|
3858
|
+
function createPackageVulnerabilitiesTool(pkgseerService) {
|
|
3859
|
+
return {
|
|
3860
|
+
name: "package_vulnerabilities",
|
|
3861
|
+
description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
|
|
3862
|
+
schema: argsSchema7,
|
|
3863
|
+
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
3864
|
+
return withErrorHandling("fetch package vulnerabilities", async () => {
|
|
3865
|
+
const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
|
|
3866
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3867
|
+
if (graphqlError)
|
|
3868
|
+
return graphqlError;
|
|
3869
|
+
if (!result.data.packageVulnerabilities) {
|
|
3870
|
+
return notFoundError(package_name, registry);
|
|
3871
|
+
}
|
|
3872
|
+
return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
// src/tools/search-package-docs.ts
|
|
3878
|
+
import { z as z6 } from "zod";
|
|
3879
|
+
var argsSchema8 = {
|
|
3880
|
+
registry: schemas.registry,
|
|
3881
|
+
package_name: schemas.packageName.describe("Name of the package to search documentation for"),
|
|
3882
|
+
keywords: z6.array(z6.string()).optional().describe("Keywords to search for; combined into a single query"),
|
|
3883
|
+
query: z6.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
|
|
3884
|
+
include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
|
|
3885
|
+
limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
|
|
3886
|
+
version: schemas.version
|
|
3887
|
+
};
|
|
3888
|
+
function createSearchPackageDocsTool(pkgseerService) {
|
|
3889
|
+
return {
|
|
3890
|
+
name: "search_package_docs",
|
|
3891
|
+
description: "Searches package documentation with keyword or freeform query support. Returns ranked results with relevance scores. Use include_snippets=true to get content excerpts showing match context.",
|
|
3892
|
+
schema: argsSchema8,
|
|
3893
|
+
handler: async ({
|
|
3894
|
+
registry,
|
|
3895
|
+
package_name,
|
|
3896
|
+
keywords,
|
|
3897
|
+
query,
|
|
3898
|
+
include_snippets,
|
|
3899
|
+
limit,
|
|
3900
|
+
version: version2
|
|
3901
|
+
}, _extra) => {
|
|
3902
|
+
return withErrorHandling("search package documentation", async () => {
|
|
3903
|
+
if (!keywords?.length && !query) {
|
|
3904
|
+
return errorResult("Either keywords or query must be provided for search");
|
|
3905
|
+
}
|
|
3906
|
+
const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
|
|
3907
|
+
keywords,
|
|
3908
|
+
query,
|
|
3909
|
+
includeSnippets: include_snippets,
|
|
3910
|
+
limit,
|
|
3911
|
+
version: version2
|
|
3912
|
+
});
|
|
3913
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3914
|
+
if (graphqlError)
|
|
3915
|
+
return graphqlError;
|
|
3916
|
+
if (!result.data.searchPackageDocs) {
|
|
3917
|
+
return errorResult(`No documentation found for ${package_name} in ${registry}`);
|
|
3918
|
+
}
|
|
3919
|
+
return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
// src/tools/search-project-docs.ts
|
|
3925
|
+
import { z as z7 } from "zod";
|
|
3926
|
+
var argsSchema9 = {
|
|
3927
|
+
project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
|
|
3928
|
+
keywords: z7.array(z7.string()).optional().describe("Keywords to search for; combined into a single query"),
|
|
3929
|
+
query: z7.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
|
|
3930
|
+
include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
|
|
3931
|
+
limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
|
|
3932
|
+
};
|
|
3933
|
+
function createSearchProjectDocsTool(deps) {
|
|
3934
|
+
const { pkgseerService, config } = deps;
|
|
3935
|
+
return {
|
|
3936
|
+
name: "search_project_docs",
|
|
3937
|
+
description: "Searches documentation across all dependencies in a PkgSeer project. Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
|
|
3938
|
+
schema: argsSchema9,
|
|
3939
|
+
handler: async ({ project, keywords, query, include_snippets, limit }, _extra) => {
|
|
3940
|
+
return withErrorHandling("search project documentation", async () => {
|
|
3941
|
+
const resolvedProject = project ?? config.project;
|
|
3942
|
+
if (!resolvedProject) {
|
|
3943
|
+
return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
|
|
3944
|
+
}
|
|
3945
|
+
if (!keywords?.length && !query) {
|
|
3946
|
+
return errorResult("Either keywords or query must be provided for search");
|
|
3947
|
+
}
|
|
3948
|
+
const result = await pkgseerService.searchProjectDocs(resolvedProject, {
|
|
3949
|
+
keywords,
|
|
3950
|
+
query,
|
|
3951
|
+
includeSnippets: include_snippets,
|
|
3952
|
+
limit
|
|
3953
|
+
});
|
|
980
3954
|
const graphqlError = handleGraphQLErrors(result.errors);
|
|
981
3955
|
if (graphqlError)
|
|
982
3956
|
return graphqlError;
|
|
983
|
-
if (!result.data.
|
|
984
|
-
return errorResult(
|
|
3957
|
+
if (!result.data.searchProjectDocs) {
|
|
3958
|
+
return errorResult(`Project not found: ${resolvedProject}`);
|
|
985
3959
|
}
|
|
986
|
-
return textResult(JSON.stringify(result.data.
|
|
3960
|
+
return textResult(JSON.stringify(result.data.searchProjectDocs, null, 2));
|
|
987
3961
|
});
|
|
988
3962
|
}
|
|
989
3963
|
};
|
|
990
3964
|
}
|
|
991
3965
|
// src/commands/mcp.ts
|
|
3966
|
+
var TOOL_FACTORIES = {
|
|
3967
|
+
package_summary: ({ pkgseerService }) => createPackageSummaryTool(pkgseerService),
|
|
3968
|
+
package_vulnerabilities: ({ pkgseerService }) => createPackageVulnerabilitiesTool(pkgseerService),
|
|
3969
|
+
package_dependencies: ({ pkgseerService }) => createPackageDependenciesTool(pkgseerService),
|
|
3970
|
+
package_quality: ({ pkgseerService }) => createPackageQualityTool(pkgseerService),
|
|
3971
|
+
compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
|
|
3972
|
+
list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
|
|
3973
|
+
fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
|
|
3974
|
+
search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
|
|
3975
|
+
search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
|
|
3976
|
+
};
|
|
3977
|
+
var PUBLIC_READ_TOOLS = [
|
|
3978
|
+
"package_summary",
|
|
3979
|
+
"package_vulnerabilities",
|
|
3980
|
+
"package_dependencies",
|
|
3981
|
+
"package_quality",
|
|
3982
|
+
"compare_packages",
|
|
3983
|
+
"list_package_docs",
|
|
3984
|
+
"fetch_package_doc",
|
|
3985
|
+
"search_package_docs"
|
|
3986
|
+
];
|
|
3987
|
+
var PROJECT_READ_TOOLS = ["search_project_docs"];
|
|
3988
|
+
var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
|
|
992
3989
|
function createMcpServer(deps) {
|
|
993
|
-
const { pkgseerService } = deps;
|
|
3990
|
+
const { pkgseerService, config } = deps;
|
|
994
3991
|
const server = new McpServer({
|
|
995
3992
|
name: "pkgseer",
|
|
996
3993
|
version: "0.1.0"
|
|
997
3994
|
});
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
createComparePackagesTool(pkgseerService)
|
|
1004
|
-
];
|
|
1005
|
-
for (const tool of tools) {
|
|
3995
|
+
const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
|
|
3996
|
+
const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
|
|
3997
|
+
for (const toolName of toolsToRegister) {
|
|
3998
|
+
const factory = TOOL_FACTORIES[toolName];
|
|
3999
|
+
const tool = factory({ pkgseerService, config });
|
|
1006
4000
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
|
|
1007
4001
|
}
|
|
1008
4002
|
return server;
|
|
@@ -1030,21 +4024,994 @@ async function startMcpServer(deps) {
|
|
|
1030
4024
|
const transport = new StdioServerTransport;
|
|
1031
4025
|
await server.connect(transport);
|
|
1032
4026
|
}
|
|
4027
|
+
function showMcpSetupInstructions(deps) {
|
|
4028
|
+
const useColors = shouldUseColors2();
|
|
4029
|
+
console.log("MCP Server Setup");
|
|
4030
|
+
console.log(`────────────────
|
|
4031
|
+
`);
|
|
4032
|
+
console.log(`Add PkgSeer to your AI assistant's MCP configuration.
|
|
4033
|
+
`);
|
|
4034
|
+
console.log(`${highlight("pkgseer mcp init", useColors)}`);
|
|
4035
|
+
console.log(dim(` Interactive setup for Cursor, Codex, or Claude Code
|
|
4036
|
+
`, useColors));
|
|
4037
|
+
console.log("Manual configuration:");
|
|
4038
|
+
console.log(` ${highlight(`${deps.baseUrl}/docs/mcp-server`, useColors)}
|
|
4039
|
+
`);
|
|
4040
|
+
console.log("Alternative: Use CLI directly (no MCP setup needed)");
|
|
4041
|
+
console.log(` ${highlight("pkgseer quickstart", useColors)}`);
|
|
4042
|
+
}
|
|
1033
4043
|
function registerMcpCommand(program) {
|
|
1034
|
-
program.command("mcp").summary("
|
|
4044
|
+
const mcpCommand = program.command("mcp").summary("Show setup instructions or start MCP server").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
|
|
4045
|
+
|
|
4046
|
+
When run interactively (TTY), shows setup instructions.
|
|
4047
|
+
When run via stdio (non-TTY), starts the MCP server.
|
|
4048
|
+
|
|
4049
|
+
Available tools: package_summary, package_vulnerabilities,
|
|
4050
|
+
package_dependencies, package_quality, compare_packages,
|
|
4051
|
+
list_package_docs, fetch_package_doc, search_package_docs,
|
|
4052
|
+
search_project_docs`).action(async () => {
|
|
4053
|
+
const deps = await createContainer();
|
|
4054
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
4055
|
+
showMcpSetupInstructions(deps);
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
await startMcpServer(deps);
|
|
4059
|
+
});
|
|
4060
|
+
mcpCommand.command("start").summary("Start MCP server (stdio mode)").description(`Start the MCP server using STDIO transport.
|
|
4061
|
+
|
|
4062
|
+
This command explicitly starts the server and is intended for use
|
|
4063
|
+
in MCP configuration files. Use 'pkgseer mcp' for interactive setup.`).action(async () => {
|
|
4064
|
+
const deps = await createContainer();
|
|
4065
|
+
await startMcpServer(deps);
|
|
4066
|
+
});
|
|
4067
|
+
registerMcpInitCommand(mcpCommand);
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/commands/pkg/compare.ts
|
|
4071
|
+
function parsePackageSpec(spec) {
|
|
4072
|
+
let registry = "npm";
|
|
4073
|
+
let rest = spec;
|
|
4074
|
+
if (spec.includes(":")) {
|
|
4075
|
+
const colonIndex = spec.indexOf(":");
|
|
4076
|
+
const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
|
|
4077
|
+
if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
|
|
4078
|
+
registry = potentialRegistry;
|
|
4079
|
+
rest = spec.slice(colonIndex + 1);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
const atIndex = rest.lastIndexOf("@");
|
|
4083
|
+
if (atIndex > 0) {
|
|
4084
|
+
return {
|
|
4085
|
+
registry,
|
|
4086
|
+
name: rest.slice(0, atIndex),
|
|
4087
|
+
version: rest.slice(atIndex + 1)
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
return { registry, name: rest };
|
|
4091
|
+
}
|
|
4092
|
+
function formatPackageComparison(comparison) {
|
|
4093
|
+
const lines = [];
|
|
4094
|
+
lines.push("\uD83D\uDCCA Package Comparison");
|
|
4095
|
+
lines.push("");
|
|
4096
|
+
if (!comparison.packages || comparison.packages.length === 0) {
|
|
4097
|
+
lines.push("No packages to compare.");
|
|
4098
|
+
return lines.join(`
|
|
4099
|
+
`);
|
|
4100
|
+
}
|
|
4101
|
+
const packages = comparison.packages.filter((p) => p != null);
|
|
4102
|
+
const maxNameLen = Math.max(...packages.map((p) => `${p.packageName}@${p.version}`.length));
|
|
4103
|
+
const header = "Package".padEnd(maxNameLen + 2);
|
|
4104
|
+
lines.push(` ${header} Quality Downloads/mo Vulns`);
|
|
4105
|
+
lines.push(` ${"─".repeat(maxNameLen + 2)} ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(5)}`);
|
|
4106
|
+
for (const pkg of packages) {
|
|
4107
|
+
const name = `${pkg.packageName}@${pkg.version}`.padEnd(maxNameLen + 2);
|
|
4108
|
+
const scoreValue = pkg.quality?.score;
|
|
4109
|
+
const quality = scoreValue != null ? `${Math.round(scoreValue > 1 ? scoreValue : scoreValue * 100)}%`.padEnd(10) : "N/A".padEnd(10);
|
|
4110
|
+
const downloads = pkg.downloadsLastMonth ? formatNumber(pkg.downloadsLastMonth).padEnd(12) : "N/A".padEnd(12);
|
|
4111
|
+
const vulns = pkg.vulnerabilityCount != null ? pkg.vulnerabilityCount === 0 ? "✅ 0" : `⚠️ ${pkg.vulnerabilityCount}` : "N/A";
|
|
4112
|
+
lines.push(` ${name} ${quality} ${downloads} ${vulns}`);
|
|
4113
|
+
}
|
|
4114
|
+
return lines.join(`
|
|
4115
|
+
`);
|
|
4116
|
+
}
|
|
4117
|
+
async function pkgCompareAction(packages, options, deps) {
|
|
4118
|
+
const { pkgseerService } = deps;
|
|
4119
|
+
if (packages.length < 2) {
|
|
4120
|
+
outputError("At least 2 packages required for comparison", options.json ?? false);
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
4123
|
+
if (packages.length > 10) {
|
|
4124
|
+
outputError("Maximum 10 packages can be compared at once", options.json ?? false);
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
const input2 = packages.map((spec) => {
|
|
4128
|
+
const parsed = parsePackageSpec(spec);
|
|
4129
|
+
return {
|
|
4130
|
+
registry: toGraphQLRegistry(parsed.registry),
|
|
4131
|
+
name: parsed.name,
|
|
4132
|
+
version: parsed.version
|
|
4133
|
+
};
|
|
4134
|
+
});
|
|
4135
|
+
const result = await pkgseerService.cliComparePackages(input2);
|
|
4136
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4137
|
+
if (!result.data.comparePackages) {
|
|
4138
|
+
outputError("Comparison failed", options.json ?? false);
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
if (options.json) {
|
|
4142
|
+
const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
|
|
4143
|
+
const slim = pkgs.map((p) => ({
|
|
4144
|
+
package: `${p.packageName}@${p.version}`,
|
|
4145
|
+
quality: p.quality?.score,
|
|
4146
|
+
downloads: p.downloadsLastMonth,
|
|
4147
|
+
vulnerabilities: p.vulnerabilityCount
|
|
4148
|
+
}));
|
|
4149
|
+
output(slim, true);
|
|
4150
|
+
} else {
|
|
4151
|
+
console.log(formatPackageComparison(result.data.comparePackages));
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
var COMPARE_DESCRIPTION = `Compare multiple packages.
|
|
4155
|
+
|
|
4156
|
+
Compares packages across quality, security, and popularity metrics.
|
|
4157
|
+
Supports cross-registry comparison.
|
|
4158
|
+
|
|
4159
|
+
Package format: [registry:]name[@version]
|
|
4160
|
+
- lodash (npm, latest)
|
|
4161
|
+
- pypi:requests (pypi, latest)
|
|
4162
|
+
- npm:express@4.18.0 (npm, specific version)
|
|
4163
|
+
|
|
4164
|
+
Examples:
|
|
4165
|
+
pkgseer pkg compare lodash underscore ramda
|
|
4166
|
+
pkgseer pkg compare npm:axios pypi:requests
|
|
4167
|
+
pkgseer pkg compare express@4.18.0 express@5.0.0 --json`;
|
|
4168
|
+
function registerPkgCompareCommand(program) {
|
|
4169
|
+
program.command("compare <packages...>").summary("Compare multiple packages").description(COMPARE_DESCRIPTION).option("--json", "Output as JSON").action(async (packages, options) => {
|
|
4170
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4171
|
+
const deps = await createContainer();
|
|
4172
|
+
await pkgCompareAction(packages, options, deps);
|
|
4173
|
+
});
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
// src/commands/pkg/deps.ts
|
|
4177
|
+
function formatPackageDependencies(data) {
|
|
4178
|
+
const lines = [];
|
|
4179
|
+
const pkg = data.package;
|
|
4180
|
+
const deps = data.dependencies;
|
|
4181
|
+
if (!pkg) {
|
|
4182
|
+
return "No package data available.";
|
|
4183
|
+
}
|
|
4184
|
+
lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.version}`);
|
|
4185
|
+
lines.push("");
|
|
4186
|
+
if (deps?.summary) {
|
|
4187
|
+
lines.push("Summary:");
|
|
4188
|
+
lines.push(` Direct dependencies: ${deps.summary.directCount ?? 0}`);
|
|
4189
|
+
if (deps.summary.uniquePackagesCount) {
|
|
4190
|
+
lines.push(` Unique packages: ${deps.summary.uniquePackagesCount}`);
|
|
4191
|
+
}
|
|
4192
|
+
lines.push("");
|
|
4193
|
+
}
|
|
4194
|
+
if (deps?.direct && deps.direct.length > 0) {
|
|
4195
|
+
lines.push(`Dependencies (${deps.direct.length}):`);
|
|
4196
|
+
for (const dep of deps.direct) {
|
|
4197
|
+
if (dep) {
|
|
4198
|
+
const type = dep.type !== "RUNTIME" ? ` [${dep.type?.toLowerCase()}]` : "";
|
|
4199
|
+
lines.push(` ${dep.name} ${dep.versionConstraint}${type}`);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
} else {
|
|
4203
|
+
lines.push("No direct dependencies.");
|
|
4204
|
+
}
|
|
4205
|
+
return lines.join(`
|
|
4206
|
+
`);
|
|
4207
|
+
}
|
|
4208
|
+
async function pkgDepsAction(packageName, options, deps) {
|
|
4209
|
+
const { pkgseerService } = deps;
|
|
4210
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4211
|
+
const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive);
|
|
4212
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4213
|
+
if (!result.data.packageDependencies) {
|
|
4214
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
if (options.json) {
|
|
4218
|
+
const data = result.data.packageDependencies;
|
|
4219
|
+
const slim = {
|
|
4220
|
+
package: `${data.package?.name}@${data.package?.version}`,
|
|
4221
|
+
directCount: data.dependencies?.summary?.directCount ?? 0,
|
|
4222
|
+
dependencies: data.dependencies?.direct?.filter((d) => d).map((d) => ({
|
|
4223
|
+
name: d.name,
|
|
4224
|
+
version: d.versionConstraint,
|
|
4225
|
+
type: d.type
|
|
4226
|
+
}))
|
|
4227
|
+
};
|
|
4228
|
+
output(slim, true);
|
|
4229
|
+
} else {
|
|
4230
|
+
console.log(formatPackageDependencies(result.data.packageDependencies));
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
var DEPS_DESCRIPTION = `Get package dependencies.
|
|
4234
|
+
|
|
4235
|
+
Lists direct dependencies and shows version constraints and
|
|
4236
|
+
dependency types (runtime, dev, optional).
|
|
4237
|
+
|
|
4238
|
+
Examples:
|
|
4239
|
+
pkgseer pkg deps express
|
|
4240
|
+
pkgseer pkg deps lodash --transitive
|
|
4241
|
+
pkgseer pkg deps requests --registry pypi --json`;
|
|
4242
|
+
function registerPkgDepsCommand(program) {
|
|
4243
|
+
program.command("deps <package>").summary("Get package dependencies").description(DEPS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("-t, --transitive", "Include transitive dependencies").option("--json", "Output as JSON").action(async (packageName, options) => {
|
|
4244
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4245
|
+
const deps = await createContainer();
|
|
4246
|
+
await pkgDepsAction(packageName, options, deps);
|
|
4247
|
+
});
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
// src/commands/pkg/info.ts
|
|
4251
|
+
function formatPackageSummary(data) {
|
|
4252
|
+
const lines = [];
|
|
4253
|
+
const pkg = data.package;
|
|
4254
|
+
if (!pkg) {
|
|
4255
|
+
return "No package data available.";
|
|
4256
|
+
}
|
|
4257
|
+
lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.latestVersion}`);
|
|
4258
|
+
lines.push("");
|
|
4259
|
+
if (pkg.description) {
|
|
4260
|
+
lines.push(pkg.description);
|
|
4261
|
+
lines.push("");
|
|
4262
|
+
}
|
|
4263
|
+
lines.push("Package Info:");
|
|
4264
|
+
lines.push(keyValueTable([
|
|
4265
|
+
["Registry", pkg.registry],
|
|
4266
|
+
["Version", pkg.latestVersion],
|
|
4267
|
+
["License", pkg.license]
|
|
4268
|
+
]));
|
|
4269
|
+
lines.push("");
|
|
4270
|
+
if (pkg.homepage || pkg.repositoryUrl) {
|
|
4271
|
+
lines.push("Links:");
|
|
4272
|
+
const links = [];
|
|
4273
|
+
if (pkg.homepage)
|
|
4274
|
+
links.push(["Homepage", pkg.homepage]);
|
|
4275
|
+
if (pkg.repositoryUrl)
|
|
4276
|
+
links.push(["Repository", pkg.repositoryUrl]);
|
|
4277
|
+
lines.push(keyValueTable(links));
|
|
4278
|
+
lines.push("");
|
|
4279
|
+
}
|
|
4280
|
+
const vulnCount = data.security?.vulnerabilityCount ?? 0;
|
|
4281
|
+
if (vulnCount > 0) {
|
|
4282
|
+
lines.push(`⚠️ Security: ${vulnCount} vulnerabilities`);
|
|
4283
|
+
lines.push("");
|
|
4284
|
+
}
|
|
4285
|
+
if (data.quickstart?.installCommand) {
|
|
4286
|
+
lines.push("Quick Start:");
|
|
4287
|
+
lines.push(` ${data.quickstart.installCommand}`);
|
|
4288
|
+
}
|
|
4289
|
+
return lines.join(`
|
|
4290
|
+
`);
|
|
4291
|
+
}
|
|
4292
|
+
async function pkgInfoAction(packageName, options, deps) {
|
|
4293
|
+
const { pkgseerService } = deps;
|
|
4294
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4295
|
+
const result = await pkgseerService.cliPackageInfo(registry, packageName);
|
|
4296
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4297
|
+
if (!result.data.packageSummary) {
|
|
4298
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
if (options.json) {
|
|
4302
|
+
const data = result.data.packageSummary;
|
|
4303
|
+
const pkg = data.package;
|
|
4304
|
+
const slim = {
|
|
4305
|
+
name: pkg?.name,
|
|
4306
|
+
version: pkg?.latestVersion,
|
|
4307
|
+
description: pkg?.description,
|
|
4308
|
+
license: pkg?.license,
|
|
4309
|
+
install: data.quickstart?.installCommand,
|
|
4310
|
+
vulnerabilities: data.security?.vulnerabilityCount ?? 0
|
|
4311
|
+
};
|
|
4312
|
+
output(slim, true);
|
|
4313
|
+
} else {
|
|
4314
|
+
console.log(formatPackageSummary(result.data.packageSummary));
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
var INFO_DESCRIPTION = `Get package summary and metadata.
|
|
4318
|
+
|
|
4319
|
+
Displays comprehensive information about a package including:
|
|
4320
|
+
- Basic metadata (version, license, description)
|
|
4321
|
+
- Download statistics
|
|
4322
|
+
- Security advisories
|
|
4323
|
+
- Quick start instructions
|
|
4324
|
+
|
|
4325
|
+
Examples:
|
|
4326
|
+
pkgseer pkg info lodash
|
|
4327
|
+
pkgseer pkg info requests --registry pypi
|
|
4328
|
+
pkgseer pkg info phoenix --registry hex --json`;
|
|
4329
|
+
function registerPkgInfoCommand(program) {
|
|
4330
|
+
program.command("info <package>").summary("Get package summary and metadata").description(INFO_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("--json", "Output as JSON").action(async (packageName, options) => {
|
|
4331
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4332
|
+
const deps = await createContainer();
|
|
4333
|
+
await pkgInfoAction(packageName, options, deps);
|
|
4334
|
+
});
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
// src/commands/pkg/quality.ts
|
|
4338
|
+
function formatPackageQuality(data) {
|
|
4339
|
+
const lines = [];
|
|
4340
|
+
const quality = data.quality;
|
|
4341
|
+
if (!quality) {
|
|
4342
|
+
return "No quality data available.";
|
|
4343
|
+
}
|
|
4344
|
+
lines.push(`\uD83D\uDCCA Quality Score: ${formatScore(quality.overallScore)} (Grade: ${quality.grade})`);
|
|
4345
|
+
lines.push("");
|
|
4346
|
+
if (quality.categories && quality.categories.length > 0) {
|
|
4347
|
+
lines.push("Category Breakdown:");
|
|
4348
|
+
for (const category of quality.categories) {
|
|
4349
|
+
if (category) {
|
|
4350
|
+
const name = (category.category || "Unknown").padEnd(20);
|
|
4351
|
+
lines.push(` ${name} ${formatScore(category.score)}`);
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
lines.push("");
|
|
4355
|
+
}
|
|
4356
|
+
return lines.join(`
|
|
4357
|
+
`);
|
|
4358
|
+
}
|
|
4359
|
+
async function pkgQualityAction(packageName, options, deps) {
|
|
4360
|
+
const { pkgseerService } = deps;
|
|
4361
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4362
|
+
const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
|
|
4363
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4364
|
+
if (!result.data.packageQuality) {
|
|
4365
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4368
|
+
if (options.json) {
|
|
4369
|
+
const quality = result.data.packageQuality.quality;
|
|
4370
|
+
const slim = {
|
|
4371
|
+
package: `${result.data.packageQuality.package?.name}@${result.data.packageQuality.package?.version}`,
|
|
4372
|
+
score: quality?.overallScore,
|
|
4373
|
+
grade: quality?.grade,
|
|
4374
|
+
categories: quality?.categories?.filter((c) => c).map((c) => ({
|
|
4375
|
+
name: c.category,
|
|
4376
|
+
score: c.score
|
|
4377
|
+
}))
|
|
4378
|
+
};
|
|
4379
|
+
output(slim, true);
|
|
4380
|
+
} else {
|
|
4381
|
+
console.log(formatPackageQuality(result.data.packageQuality));
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
var QUALITY_DESCRIPTION = `Get package quality score and breakdown.
|
|
4385
|
+
|
|
4386
|
+
Analyzes package quality across multiple dimensions:
|
|
4387
|
+
- Maintenance and activity
|
|
4388
|
+
- Documentation coverage
|
|
4389
|
+
- Security practices
|
|
4390
|
+
- Community engagement
|
|
4391
|
+
|
|
4392
|
+
Examples:
|
|
4393
|
+
pkgseer pkg quality lodash
|
|
4394
|
+
pkgseer pkg quality express -v 4.18.0
|
|
4395
|
+
pkgseer pkg quality requests --registry pypi --json`;
|
|
4396
|
+
function registerPkgQualityCommand(program) {
|
|
4397
|
+
program.command("quality <package>").summary("Get package quality score").description(QUALITY_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
|
|
4398
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4399
|
+
const deps = await createContainer();
|
|
4400
|
+
await pkgQualityAction(packageName, options, deps);
|
|
4401
|
+
});
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
// src/commands/pkg/vulns.ts
|
|
4405
|
+
function getSeverityLabel(score) {
|
|
4406
|
+
if (score == null)
|
|
4407
|
+
return "UNKNOWN";
|
|
4408
|
+
if (score >= 9)
|
|
4409
|
+
return "CRITICAL";
|
|
4410
|
+
if (score >= 7)
|
|
4411
|
+
return "HIGH";
|
|
4412
|
+
if (score >= 4)
|
|
4413
|
+
return "MEDIUM";
|
|
4414
|
+
return "LOW";
|
|
4415
|
+
}
|
|
4416
|
+
function formatPackageVulnerabilities(data) {
|
|
4417
|
+
const lines = [];
|
|
4418
|
+
const pkg = data.package;
|
|
4419
|
+
const security = data.security;
|
|
4420
|
+
if (!pkg) {
|
|
4421
|
+
return "No package data available.";
|
|
4422
|
+
}
|
|
4423
|
+
lines.push(`\uD83D\uDD12 Security Report: ${pkg.name}@${pkg.version}`);
|
|
4424
|
+
lines.push("");
|
|
4425
|
+
if (!security?.vulnerabilities || security.vulnerabilities.length === 0) {
|
|
4426
|
+
lines.push("✅ No known vulnerabilities!");
|
|
4427
|
+
return lines.join(`
|
|
4428
|
+
`);
|
|
4429
|
+
}
|
|
4430
|
+
const bySeverity = security.vulnerabilities.reduce((acc, v) => {
|
|
4431
|
+
if (v) {
|
|
4432
|
+
const label = getSeverityLabel(v.severityScore);
|
|
4433
|
+
acc[label] = (acc[label] || 0) + 1;
|
|
4434
|
+
}
|
|
4435
|
+
return acc;
|
|
4436
|
+
}, {});
|
|
4437
|
+
lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
|
|
4438
|
+
for (const [severity, count] of Object.entries(bySeverity)) {
|
|
4439
|
+
lines.push(` ${formatSeverity(severity)}: ${count}`);
|
|
4440
|
+
}
|
|
4441
|
+
lines.push("");
|
|
4442
|
+
lines.push("Details:");
|
|
4443
|
+
for (const vuln of security.vulnerabilities) {
|
|
4444
|
+
if (!vuln)
|
|
4445
|
+
continue;
|
|
4446
|
+
lines.push("");
|
|
4447
|
+
lines.push(` ${formatSeverity(getSeverityLabel(vuln.severityScore))}`);
|
|
4448
|
+
lines.push(` ${vuln.summary || vuln.osvId}`);
|
|
4449
|
+
lines.push(` ID: ${vuln.osvId}`);
|
|
4450
|
+
if (vuln.fixedInVersions && vuln.fixedInVersions.length > 0) {
|
|
4451
|
+
lines.push(` Fixed in: ${vuln.fixedInVersions.join(", ")}`);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
return lines.join(`
|
|
4455
|
+
`);
|
|
4456
|
+
}
|
|
4457
|
+
async function pkgVulnsAction(packageName, options, deps) {
|
|
4458
|
+
const { pkgseerService } = deps;
|
|
4459
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4460
|
+
const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
|
|
4461
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4462
|
+
if (!result.data.packageVulnerabilities) {
|
|
4463
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
if (options.json) {
|
|
4467
|
+
const data = result.data.packageVulnerabilities;
|
|
4468
|
+
const vulns = data.security?.vulnerabilities ?? [];
|
|
4469
|
+
const slim = {
|
|
4470
|
+
package: `${data.package?.name}@${data.package?.version}`,
|
|
4471
|
+
count: data.security?.vulnerabilityCount ?? 0,
|
|
4472
|
+
vulnerabilities: vulns.filter((v) => v).map((v) => ({
|
|
4473
|
+
id: v.osvId,
|
|
4474
|
+
severity: v.severityScore,
|
|
4475
|
+
summary: v.summary,
|
|
4476
|
+
fixed: v.fixedInVersions
|
|
4477
|
+
}))
|
|
4478
|
+
};
|
|
4479
|
+
output(slim, true);
|
|
4480
|
+
} else {
|
|
4481
|
+
console.log(formatPackageVulnerabilities(result.data.packageVulnerabilities));
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
var VULNS_DESCRIPTION = `Check package for security vulnerabilities.
|
|
4485
|
+
|
|
4486
|
+
Scans for known security vulnerabilities and provides:
|
|
4487
|
+
- Severity ratings (critical, high, medium, low)
|
|
4488
|
+
- CVE identifiers
|
|
4489
|
+
- Affected version ranges
|
|
4490
|
+
- Upgrade recommendations
|
|
4491
|
+
|
|
4492
|
+
Examples:
|
|
4493
|
+
pkgseer pkg vulns lodash
|
|
4494
|
+
pkgseer pkg vulns express -v 4.17.0
|
|
4495
|
+
pkgseer pkg vulns requests --registry pypi --json`;
|
|
4496
|
+
function registerPkgVulnsCommand(program) {
|
|
4497
|
+
program.command("vulns <package>").summary("Check for security vulnerabilities").description(VULNS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
|
|
4498
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4499
|
+
const deps = await createContainer();
|
|
4500
|
+
await pkgVulnsAction(packageName, options, deps);
|
|
4501
|
+
});
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
// src/commands/project/detect.ts
|
|
4505
|
+
function matchManifestsWithConfig(detectedGroups, existingManifests) {
|
|
4506
|
+
const fileToConfig = new Map;
|
|
4507
|
+
for (const group of existingManifests) {
|
|
4508
|
+
for (const file of group.files) {
|
|
4509
|
+
fileToConfig.set(file, {
|
|
4510
|
+
label: group.label,
|
|
4511
|
+
allow_mix_deps: group.allow_mix_deps
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
const labelToFiles = new Map;
|
|
4516
|
+
const labelToConfig = new Map;
|
|
4517
|
+
for (const detectedGroup of detectedGroups) {
|
|
4518
|
+
for (const manifest of detectedGroup.manifests) {
|
|
4519
|
+
const existingConfig = fileToConfig.get(manifest.relativePath);
|
|
4520
|
+
let label;
|
|
4521
|
+
const hasEcosystemSuffix = detectedGroup.label.endsWith("-npm") || detectedGroup.label.endsWith("-hex") || detectedGroup.label.endsWith("-pypi");
|
|
4522
|
+
if (hasEcosystemSuffix) {
|
|
4523
|
+
label = detectedGroup.label;
|
|
4524
|
+
} else {
|
|
4525
|
+
label = existingConfig?.label ?? detectedGroup.label;
|
|
4526
|
+
}
|
|
4527
|
+
const allowMixDeps = existingConfig?.allow_mix_deps;
|
|
4528
|
+
if (!labelToConfig.has(label)) {
|
|
4529
|
+
labelToConfig.set(label, {
|
|
4530
|
+
files: new Set,
|
|
4531
|
+
allow_mix_deps: allowMixDeps
|
|
4532
|
+
});
|
|
4533
|
+
}
|
|
4534
|
+
const config = labelToConfig.get(label);
|
|
4535
|
+
config.files.add(manifest.relativePath);
|
|
4536
|
+
if (allowMixDeps) {
|
|
4537
|
+
config.allow_mix_deps = true;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
const result = [];
|
|
4542
|
+
for (const [label, config] of labelToConfig.entries()) {
|
|
4543
|
+
const fileArray = Array.from(config.files);
|
|
4544
|
+
const hasNewFiles = fileArray.some((file) => !fileToConfig.has(file));
|
|
4545
|
+
const hasChangedLabels = fileArray.some((file) => {
|
|
4546
|
+
const oldConfig = fileToConfig.get(file);
|
|
4547
|
+
return oldConfig !== undefined && oldConfig.label !== label;
|
|
4548
|
+
});
|
|
4549
|
+
result.push({
|
|
4550
|
+
label,
|
|
4551
|
+
files: fileArray.sort(),
|
|
4552
|
+
isNew: hasNewFiles,
|
|
4553
|
+
changedLabel: hasChangedLabels ? label : undefined,
|
|
4554
|
+
allow_mix_deps: config.allow_mix_deps
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
return result.sort((a, b) => {
|
|
4558
|
+
if (a.label.startsWith("root")) {
|
|
4559
|
+
if (b.label.startsWith("root")) {
|
|
4560
|
+
return a.label.localeCompare(b.label);
|
|
4561
|
+
}
|
|
4562
|
+
return -1;
|
|
4563
|
+
}
|
|
4564
|
+
if (b.label.startsWith("root")) {
|
|
4565
|
+
return 1;
|
|
4566
|
+
}
|
|
4567
|
+
return a.label.localeCompare(b.label);
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
async function projectDetectAction(options, deps) {
|
|
4571
|
+
const { configService, fileSystemService, promptService, shellService } = deps;
|
|
4572
|
+
const projectConfig = await configService.loadProjectConfig();
|
|
4573
|
+
if (!projectConfig?.config.project) {
|
|
4574
|
+
console.error(`✗ No project is configured in pkgseer.yml`);
|
|
4575
|
+
console.log(`
|
|
4576
|
+
To get started, run: pkgseer project init`);
|
|
4577
|
+
console.log(` This will create a project and detect your manifest files.`);
|
|
4578
|
+
process.exit(1);
|
|
4579
|
+
}
|
|
4580
|
+
const projectName = projectConfig.config.project;
|
|
4581
|
+
const existingManifests = projectConfig.config.manifests ?? [];
|
|
4582
|
+
console.log(`Scanning for manifest files in project "${projectName}"...
|
|
4583
|
+
`);
|
|
4584
|
+
const cwd = fileSystemService.getCwd();
|
|
4585
|
+
const detectedGroups = await detectAndGroupManifests(cwd, fileSystemService, {
|
|
4586
|
+
maxDepth: options.maxDepth ?? 3
|
|
4587
|
+
});
|
|
4588
|
+
if (detectedGroups.length === 0) {
|
|
4589
|
+
console.log("No manifest files were found in the current directory.");
|
|
4590
|
+
if (existingManifests.length > 0) {
|
|
4591
|
+
console.log(`
|
|
4592
|
+
Your existing configuration in pkgseer.yml will remain unchanged.`);
|
|
4593
|
+
console.log(` Tip: Make sure you're running this command from the project root directory.`);
|
|
4594
|
+
} else {
|
|
4595
|
+
console.log(`
|
|
4596
|
+
Tip: Manifest files like package.json, requirements.txt, or pyproject.toml should be in the current directory or subdirectories.`);
|
|
4597
|
+
}
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
const suggestedManifests = matchManifestsWithConfig(detectedGroups, existingManifests);
|
|
4601
|
+
console.log("Current configuration in pkgseer.yml:");
|
|
4602
|
+
if (existingManifests.length === 0) {
|
|
4603
|
+
console.log(" (no manifests configured yet)");
|
|
4604
|
+
} else {
|
|
4605
|
+
for (const group of existingManifests) {
|
|
4606
|
+
console.log(` Label: ${group.label}`);
|
|
4607
|
+
for (const file of group.files) {
|
|
4608
|
+
console.log(` ${file}`);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
console.log(`
|
|
4613
|
+
Suggested configuration:`);
|
|
4614
|
+
for (const group of suggestedManifests) {
|
|
4615
|
+
const markers = [];
|
|
4616
|
+
if (group.isNew)
|
|
4617
|
+
markers.push("new");
|
|
4618
|
+
if (group.changedLabel)
|
|
4619
|
+
markers.push("label changed");
|
|
4620
|
+
const markerStr = markers.length > 0 ? ` (${markers.join(", ")})` : "";
|
|
4621
|
+
console.log(` Label: ${group.label}${markerStr}`);
|
|
4622
|
+
for (const file of group.files) {
|
|
4623
|
+
const wasInConfig = existingManifests.some((g) => g.files.includes(file));
|
|
4624
|
+
const prefix = wasInConfig ? " " : " + ";
|
|
4625
|
+
console.log(`${prefix}${file}`);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
|
|
4629
|
+
const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
|
|
4630
|
+
let allowMixDeps = false;
|
|
4631
|
+
if (hasHexInSuggested) {
|
|
4632
|
+
const existingHasMixDeps = existingManifests.some((g) => g.allow_mix_deps === true);
|
|
4633
|
+
if (!existingHasMixDeps) {
|
|
4634
|
+
console.log(`
|
|
4635
|
+
Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`);
|
|
4636
|
+
console.log(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`);
|
|
4637
|
+
allowMixDeps = await promptService.confirm(`
|
|
4638
|
+
Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
|
|
4639
|
+
} else {
|
|
4640
|
+
allowMixDeps = true;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
const finalManifests = suggestedManifests.map((g) => {
|
|
4644
|
+
const hasHexInGroup = g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
4645
|
+
return {
|
|
4646
|
+
label: g.label,
|
|
4647
|
+
files: g.files,
|
|
4648
|
+
isNew: g.isNew,
|
|
4649
|
+
changedLabel: g.changedLabel,
|
|
4650
|
+
...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {},
|
|
4651
|
+
...g.allow_mix_deps !== undefined ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4652
|
+
};
|
|
4653
|
+
});
|
|
4654
|
+
const hasChanges = finalManifests.length !== existingManifests.length || finalManifests.some((g) => g.isNew || g.changedLabel) || existingManifests.some((existing) => {
|
|
4655
|
+
const suggested = finalManifests.find((s) => s.label === existing.label);
|
|
4656
|
+
if (!suggested)
|
|
4657
|
+
return true;
|
|
4658
|
+
const existingFiles = new Set(existing.files);
|
|
4659
|
+
const suggestedFiles = new Set(suggested.files);
|
|
4660
|
+
return existingFiles.size !== suggestedFiles.size || !Array.from(existingFiles).every((f) => suggestedFiles.has(f));
|
|
4661
|
+
}) || finalManifests.some((g) => {
|
|
4662
|
+
const existing = existingManifests.find((e) => e.label === g.label);
|
|
4663
|
+
return existing?.allow_mix_deps !== g.allow_mix_deps;
|
|
4664
|
+
});
|
|
4665
|
+
if (!hasChanges) {
|
|
4666
|
+
console.log(`
|
|
4667
|
+
✓ Your configuration is already up to date!`);
|
|
4668
|
+
console.log(`
|
|
4669
|
+
All detected manifest files match your current pkgseer.yml configuration.`);
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (options.update) {
|
|
4673
|
+
await configService.writeProjectConfig({
|
|
4674
|
+
project: projectName,
|
|
4675
|
+
manifests: finalManifests.map((g) => ({
|
|
4676
|
+
label: g.label,
|
|
4677
|
+
files: g.files,
|
|
4678
|
+
...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4679
|
+
}))
|
|
4680
|
+
});
|
|
4681
|
+
console.log(`
|
|
4682
|
+
✓ Configuration updated successfully!`);
|
|
4683
|
+
console.log(`
|
|
4684
|
+
Your pkgseer.yml has been updated with the detected manifest files.`);
|
|
4685
|
+
console.log(` Run 'pkgseer project upload' to upload them to your project.`);
|
|
4686
|
+
} else {
|
|
4687
|
+
const shouldUpdate = await promptService.confirm(`
|
|
4688
|
+
Would you like to update pkgseer.yml with these changes?`, false);
|
|
4689
|
+
if (shouldUpdate) {
|
|
4690
|
+
await configService.writeProjectConfig({
|
|
4691
|
+
project: projectName,
|
|
4692
|
+
manifests: finalManifests.map((g) => ({
|
|
4693
|
+
label: g.label,
|
|
4694
|
+
files: g.files,
|
|
4695
|
+
...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4696
|
+
}))
|
|
4697
|
+
});
|
|
4698
|
+
console.log(`
|
|
4699
|
+
✓ Configuration updated successfully!`);
|
|
4700
|
+
console.log(`
|
|
4701
|
+
Your pkgseer.yml has been updated. Run 'pkgseer project upload' to upload the manifests.`);
|
|
4702
|
+
} else {
|
|
4703
|
+
console.log(`
|
|
4704
|
+
Configuration was not updated.`);
|
|
4705
|
+
console.log(` To apply these changes automatically, run: pkgseer project detect --update`);
|
|
4706
|
+
console.log(` Or manually edit pkgseer.yml and then run: pkgseer project upload`);
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
var DETECT_DESCRIPTION = `Detect manifest files and update your project configuration.
|
|
4711
|
+
|
|
4712
|
+
This command scans your project directory for manifest files (like
|
|
4713
|
+
package.json, requirements.txt, etc.) and compares them with your
|
|
4714
|
+
current pkgseer.yml configuration. It will:
|
|
4715
|
+
|
|
4716
|
+
• Preserve existing labels for files you've already configured
|
|
4717
|
+
• Suggest new labels for newly detected files
|
|
4718
|
+
• Show you what would change before updating
|
|
4719
|
+
|
|
4720
|
+
Perfect for when you add new manifest files or reorganize your
|
|
4721
|
+
project structure. Run with --update to automatically apply changes.`;
|
|
4722
|
+
function registerProjectDetectCommand(program) {
|
|
4723
|
+
program.command("detect").summary("Detect manifest files and suggest config updates").description(DETECT_DESCRIPTION).option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).option("--update", "Automatically update pkgseer.yml without prompting", false).action(async (options) => {
|
|
4724
|
+
const deps = await createContainer();
|
|
4725
|
+
await projectDetectAction({ maxDepth: options.maxDepth, update: options.update }, {
|
|
4726
|
+
configService: deps.configService,
|
|
4727
|
+
fileSystemService: deps.fileSystemService,
|
|
4728
|
+
promptService: deps.promptService,
|
|
4729
|
+
shellService: deps.shellService
|
|
4730
|
+
});
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4733
|
+
// src/commands/project/upload.ts
|
|
4734
|
+
async function projectUploadAction(options, deps) {
|
|
4735
|
+
const {
|
|
4736
|
+
projectService,
|
|
4737
|
+
configService,
|
|
4738
|
+
fileSystemService,
|
|
4739
|
+
gitService,
|
|
4740
|
+
shellService,
|
|
4741
|
+
promptService,
|
|
4742
|
+
baseUrl
|
|
4743
|
+
} = deps;
|
|
4744
|
+
const useColors = shouldUseColors2();
|
|
4745
|
+
const tokenData = await checkProjectWriteScope(configService, baseUrl);
|
|
4746
|
+
if (!tokenData) {
|
|
4747
|
+
console.error(error(`Authentication required. Please run 'pkgseer login'.`, useColors));
|
|
4748
|
+
console.log(`
|
|
4749
|
+
To fix this:`);
|
|
4750
|
+
console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
|
|
4751
|
+
console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
|
|
4752
|
+
console.log(`
|
|
4753
|
+
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
4754
|
+
process.exit(1);
|
|
4755
|
+
}
|
|
4756
|
+
const projectConfig = await configService.loadProjectConfig();
|
|
4757
|
+
if (!projectConfig?.config.project) {
|
|
4758
|
+
console.error(error(`No project is configured in pkgseer.yml`, useColors));
|
|
4759
|
+
console.log(`
|
|
4760
|
+
To get started, run: ${highlight(`pkgseer project init`, useColors)}`);
|
|
4761
|
+
console.log(` This will create a project and detect your manifest files.`);
|
|
4762
|
+
process.exit(1);
|
|
4763
|
+
}
|
|
4764
|
+
const projectName = projectConfig.config.project;
|
|
4765
|
+
let manifests = projectConfig.config.manifests ?? [];
|
|
4766
|
+
const configDir = fileSystemService.getDirname(projectConfig.path);
|
|
4767
|
+
if (manifests.length === 0) {
|
|
4768
|
+
console.error(error(`No manifest files are configured in pkgseer.yml`, useColors));
|
|
4769
|
+
console.log(`
|
|
4770
|
+
To add manifest files, you can:`);
|
|
4771
|
+
console.log(` 1. Run: ${highlight(`pkgseer project detect`, useColors)}`);
|
|
4772
|
+
console.log(` This will automatically detect and configure manifest files`);
|
|
4773
|
+
console.log(` 2. Or manually edit pkgseer.yml to add manifest files`);
|
|
4774
|
+
console.log(`
|
|
4775
|
+
Then run this command again to upload them.`);
|
|
4776
|
+
process.exit(1);
|
|
4777
|
+
}
|
|
4778
|
+
const needsPermission = manifests.some((group) => {
|
|
4779
|
+
const hasHexFiles = group.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
4780
|
+
return hasHexFiles && group.allow_mix_deps !== true;
|
|
4781
|
+
});
|
|
4782
|
+
if (needsPermission) {
|
|
4783
|
+
console.log(dim(`
|
|
4784
|
+
Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
|
|
4785
|
+
console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
|
|
4786
|
+
const allowMixDeps = await promptService.confirm(`
|
|
4787
|
+
Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
|
|
4788
|
+
if (!allowMixDeps) {
|
|
4789
|
+
console.log(dim(`
|
|
4790
|
+
Upload cancelled. Hex manifest files cannot be uploaded directly.`, useColors));
|
|
4791
|
+
console.log(dim(` To upload hex manifests, you must allow running "mix deps --all" and "mix deps.tree".`, useColors));
|
|
4792
|
+
process.exit(0);
|
|
4793
|
+
}
|
|
4794
|
+
manifests = manifests.map((group) => {
|
|
4795
|
+
const hasHexFiles = group.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
4796
|
+
if (hasHexFiles) {
|
|
4797
|
+
return { ...group, allow_mix_deps: true };
|
|
4798
|
+
}
|
|
4799
|
+
return group;
|
|
4800
|
+
});
|
|
4801
|
+
await configService.writeProjectConfig({
|
|
4802
|
+
project: projectName,
|
|
4803
|
+
manifests
|
|
4804
|
+
});
|
|
4805
|
+
console.log(success(`Configuration updated: ${highlight("allow_mix_deps", useColors)} permission saved`, useColors));
|
|
4806
|
+
}
|
|
4807
|
+
let branch = options.branch;
|
|
4808
|
+
if (!branch) {
|
|
4809
|
+
branch = await gitService.getCurrentBranch() ?? "main";
|
|
4810
|
+
}
|
|
4811
|
+
console.log(`Uploading manifest files to project ${highlight(projectName, useColors)}`);
|
|
4812
|
+
console.log(`Branch: ${highlight(branch, useColors)}
|
|
4813
|
+
`);
|
|
4814
|
+
const basePath = configDir;
|
|
4815
|
+
let totalSucceeded = 0;
|
|
4816
|
+
let totalFailed = 0;
|
|
4817
|
+
for (const manifestGroup of manifests) {
|
|
4818
|
+
console.log(`Uploading ${manifestGroup.label}...`);
|
|
4819
|
+
try {
|
|
4820
|
+
const hexFiles = manifestGroup.files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
4821
|
+
const hasHexManifests = hexFiles.length > 0;
|
|
4822
|
+
const isHexGroup = manifestGroup.allow_mix_deps === true;
|
|
4823
|
+
if (hasHexManifests && !isHexGroup) {
|
|
4824
|
+
console.log(` ${dim(`Skipping hex files. Please run 'pkgseer project detect' to update configuration.`, useColors)}`);
|
|
4825
|
+
totalFailed += hexFiles.length;
|
|
4826
|
+
continue;
|
|
4827
|
+
}
|
|
4828
|
+
let allFiles;
|
|
4829
|
+
try {
|
|
4830
|
+
allFiles = await processManifestFiles({
|
|
4831
|
+
files: manifestGroup.files,
|
|
4832
|
+
basePath,
|
|
4833
|
+
hasHexManifests,
|
|
4834
|
+
allowMixDeps: isHexGroup,
|
|
4835
|
+
fileSystemService,
|
|
4836
|
+
shellService
|
|
4837
|
+
});
|
|
4838
|
+
} catch (processError) {
|
|
4839
|
+
const errorMessage = processError instanceof Error ? processError.message : String(processError);
|
|
4840
|
+
let userMessage = `Failed to process files for ${manifestGroup.label}: ${errorMessage}`;
|
|
4841
|
+
if (hasHexManifests) {
|
|
4842
|
+
if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
|
|
4843
|
+
userMessage = `Failed to process hex manifest files for ${manifestGroup.label}.
|
|
4844
|
+
` + ` Error: ${errorMessage}
|
|
4845
|
+
` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
|
|
4846
|
+
} else if (errorMessage.includes("Failed to generate dependencies")) {
|
|
4847
|
+
userMessage = errorMessage;
|
|
4848
|
+
} else {
|
|
4849
|
+
userMessage = `Failed to process hex manifest files for ${manifestGroup.label}.
|
|
4850
|
+
` + ` Error: ${errorMessage}
|
|
4851
|
+
` + ` Make sure the directory contains a valid Elixir project and dependencies can be resolved.`;
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
console.error(error(userMessage, useColors));
|
|
4855
|
+
totalFailed += manifestGroup.files.length;
|
|
4856
|
+
continue;
|
|
4857
|
+
}
|
|
4858
|
+
const validFiles = allFiles.filter((f) => f !== null);
|
|
4859
|
+
if (validFiles.length === 0) {
|
|
4860
|
+
console.log(` ${dim(`(no files to upload for this group)`, useColors)}`);
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
const uploadResult = await projectService.uploadManifests({
|
|
4864
|
+
project: projectName,
|
|
4865
|
+
branch,
|
|
4866
|
+
label: manifestGroup.label,
|
|
4867
|
+
files: validFiles
|
|
4868
|
+
});
|
|
4869
|
+
for (const result of uploadResult.results) {
|
|
4870
|
+
if (result.status === "success") {
|
|
4871
|
+
const depsCount = result.dependencies_count ?? 0;
|
|
4872
|
+
console.log(` ${success(`${highlight(result.filename, useColors)}`, useColors)} ${dim(`(${depsCount} dependencies)`, useColors)}`);
|
|
4873
|
+
totalSucceeded++;
|
|
4874
|
+
} else {
|
|
4875
|
+
console.log(` ${error(`${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
|
|
4876
|
+
totalFailed++;
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
} catch (uploadError) {
|
|
4880
|
+
console.error(error(`Failed to upload ${manifestGroup.label}: ${uploadError instanceof Error ? uploadError.message : uploadError}`, useColors));
|
|
4881
|
+
totalFailed += manifestGroup.files.length;
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
if (totalSucceeded > 0 || totalFailed > 0) {
|
|
4885
|
+
const successText = totalSucceeded > 0 ? `${totalSucceeded} file${totalSucceeded === 1 ? "" : "s"} succeeded` : "";
|
|
4886
|
+
const failText = totalFailed > 0 ? `${totalFailed} file${totalFailed === 1 ? "" : "s"} failed` : "";
|
|
4887
|
+
const statusText = [successText, failText].filter(Boolean).join(", ");
|
|
4888
|
+
console.log(`
|
|
4889
|
+
${success(`Upload complete: ${statusText}`, useColors)}`);
|
|
4890
|
+
}
|
|
4891
|
+
if (totalFailed > 0) {
|
|
4892
|
+
console.log(`
|
|
4893
|
+
Some files failed to upload. Please check the errors above and try again.`);
|
|
4894
|
+
console.log(` Tip: Make sure all files exist and are readable, and that you're authenticated with proper scopes.`);
|
|
4895
|
+
console.log(` Check your scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
4896
|
+
process.exit(1);
|
|
4897
|
+
}
|
|
4898
|
+
console.log(`
|
|
4899
|
+
Your manifest files have been uploaded successfully!`);
|
|
4900
|
+
console.log(` View your project dependencies and insights in the PkgSeer dashboard.`);
|
|
4901
|
+
}
|
|
4902
|
+
var UPLOAD_DESCRIPTION = `Upload manifest files to your project.
|
|
4903
|
+
|
|
4904
|
+
This command reads the manifest files configured in pkgseer.yml
|
|
4905
|
+
and uploads them to your project. It will:
|
|
4906
|
+
|
|
4907
|
+
• Upload each manifest group with its configured label
|
|
4908
|
+
• Use your current git branch (or 'main' if not in a git repo)
|
|
4909
|
+
• Show progress and results for each file
|
|
4910
|
+
|
|
4911
|
+
Make sure you've configured manifest files first (using 'pkgseer
|
|
4912
|
+
project init' or 'pkgseer project detect'), and that you're
|
|
4913
|
+
authenticated (run 'pkgseer login' if needed).`;
|
|
4914
|
+
function registerProjectUploadCommand(program) {
|
|
4915
|
+
program.command("upload").summary("Upload manifest files to a project").description(UPLOAD_DESCRIPTION).option("--branch <branch>", "Git branch name (defaults to current branch or 'main')").action(async (options) => {
|
|
4916
|
+
const deps = await createContainer();
|
|
4917
|
+
await projectUploadAction(options, {
|
|
4918
|
+
projectService: deps.projectService,
|
|
4919
|
+
configService: deps.configService,
|
|
4920
|
+
fileSystemService: deps.fileSystemService,
|
|
4921
|
+
gitService: deps.gitService,
|
|
4922
|
+
shellService: deps.shellService,
|
|
4923
|
+
promptService: deps.promptService,
|
|
4924
|
+
baseUrl: deps.baseUrl
|
|
4925
|
+
});
|
|
4926
|
+
});
|
|
4927
|
+
}
|
|
4928
|
+
// src/commands/quickstart.ts
|
|
4929
|
+
var QUICKSTART_TEXT = `# PkgSeer CLI - Quick Reference for AI Agents
|
|
4930
|
+
|
|
4931
|
+
PkgSeer provides package intelligence for npm, PyPI, and Hex registries.
|
|
4932
|
+
|
|
4933
|
+
## Package Commands (pkgseer pkg)
|
|
4934
|
+
|
|
4935
|
+
### Get package info
|
|
4936
|
+
\`pkgseer pkg info <package> [-r npm|pypi|hex] [--json]\`
|
|
4937
|
+
Returns: metadata, versions, security advisories, quickstart
|
|
4938
|
+
|
|
4939
|
+
### Check quality score
|
|
4940
|
+
\`pkgseer pkg quality <package> [-r registry] [-v pkg-version] [--json]\`
|
|
4941
|
+
Returns: overall score, category breakdown, rule details
|
|
4942
|
+
|
|
4943
|
+
### List dependencies
|
|
4944
|
+
\`pkgseer pkg deps <package> [-r registry] [-v pkg-version] [-t] [-d depth] [--json]\`
|
|
4945
|
+
Returns: direct deps, transitive count, dependency tree (with -t)
|
|
4946
|
+
|
|
4947
|
+
### Check vulnerabilities
|
|
4948
|
+
\`pkgseer pkg vulns <package> [-r registry] [-v pkg-version] [--json]\`
|
|
4949
|
+
Returns: CVEs, severity, affected versions, upgrade guidance
|
|
4950
|
+
|
|
4951
|
+
### Compare packages
|
|
4952
|
+
\`pkgseer pkg compare <pkg1> <pkg2> [...] [--json]\`
|
|
4953
|
+
Format: [registry:]name[@version] (e.g., npm:lodash, pypi:requests@2.31.0)
|
|
4954
|
+
Returns: side-by-side quality, downloads, vulnerabilities
|
|
4955
|
+
|
|
4956
|
+
## Documentation Commands (pkgseer docs)
|
|
1035
4957
|
|
|
1036
|
-
|
|
1037
|
-
|
|
4958
|
+
### List doc pages
|
|
4959
|
+
\`pkgseer docs list <package> [-r registry] [-v pkg-version] [--json]\`
|
|
4960
|
+
Returns: page titles, IDs (slugs), word counts
|
|
1038
4961
|
|
|
4962
|
+
### Get doc page
|
|
4963
|
+
\`pkgseer docs get <package>/<page-id> [<package>/<page-id> ...] [-r registry] [-v pkg-version] [--json]\`
|
|
4964
|
+
Returns: full page content (markdown), breadcrumbs, source URL
|
|
4965
|
+
Supports multiple pages: \`pkgseer docs get express/readme express/api\`
|
|
4966
|
+
|
|
4967
|
+
### Search docs
|
|
4968
|
+
\`pkgseer docs search "<query>" [-p package] [-r registry] [--json]\`
|
|
4969
|
+
\`pkgseer docs search --keywords term1,term2 [-s] [-l limit]\`
|
|
4970
|
+
Default: searches project docs (if project configured)
|
|
4971
|
+
With -p: searches specific package docs
|
|
4972
|
+
Returns: ranked results with relevance scores
|
|
4973
|
+
|
|
4974
|
+
## Tips for AI Agents
|
|
4975
|
+
|
|
4976
|
+
1. Use \`--json\` for structured data parsing
|
|
4977
|
+
2. Default registry is npm; use \`-r pypi\` or \`-r hex\` for others
|
|
4978
|
+
3. Version defaults to latest; use \`-v <version>\` for specific version
|
|
4979
|
+
4. For docs search, configure project in pkgseer.yml for project-wide search
|
|
4980
|
+
5. Compare up to 10 packages at once with \`pkg compare\`
|
|
4981
|
+
|
|
4982
|
+
## MCP Server
|
|
4983
|
+
|
|
4984
|
+
For Model Context Protocol integration:
|
|
4985
|
+
\`pkgseer mcp\`
|
|
4986
|
+
|
|
4987
|
+
Add to MCP config:
|
|
4988
|
+
{
|
|
1039
4989
|
"pkgseer": {
|
|
1040
4990
|
"command": "pkgseer",
|
|
1041
4991
|
"args": ["mcp"]
|
|
1042
4992
|
}
|
|
4993
|
+
}
|
|
4994
|
+
`;
|
|
4995
|
+
function quickstartAction(options) {
|
|
4996
|
+
if (options.json) {
|
|
4997
|
+
console.log(JSON.stringify({
|
|
4998
|
+
version,
|
|
4999
|
+
quickstart: QUICKSTART_TEXT
|
|
5000
|
+
}));
|
|
5001
|
+
} else {
|
|
5002
|
+
console.log(QUICKSTART_TEXT);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
var QUICKSTART_DESCRIPTION = `Show quick reference for AI agents.
|
|
1043
5006
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
5007
|
+
Displays a concise guide to pkgseer CLI commands optimized
|
|
5008
|
+
for LLM agents. Includes command syntax, options, and tips
|
|
5009
|
+
for effective usage.
|
|
5010
|
+
|
|
5011
|
+
Use --json for structured output.`;
|
|
5012
|
+
function registerQuickstartCommand(program) {
|
|
5013
|
+
program.command("quickstart").summary("Show quick reference for AI agents").description(QUICKSTART_DESCRIPTION).option("--json", "Output as JSON").action((options) => {
|
|
5014
|
+
quickstartAction(options);
|
|
1048
5015
|
});
|
|
1049
5016
|
}
|
|
1050
5017
|
|
|
@@ -1052,13 +5019,44 @@ package_dependencies, package_quality, compare_packages`).action(async () => {
|
|
|
1052
5019
|
var program = new Command;
|
|
1053
5020
|
program.name("pkgseer").description("Package intelligence for your AI assistant").version(version).addHelpText("after", `
|
|
1054
5021
|
Getting started:
|
|
1055
|
-
pkgseer
|
|
1056
|
-
pkgseer
|
|
5022
|
+
pkgseer init Interactive setup wizard (project + MCP)
|
|
5023
|
+
pkgseer login Authenticate with your account
|
|
5024
|
+
pkgseer quickstart Quick reference for AI agents
|
|
5025
|
+
|
|
5026
|
+
Package commands:
|
|
5027
|
+
pkgseer pkg info <package> Get package summary
|
|
5028
|
+
pkgseer pkg vulns <package> Check vulnerabilities
|
|
5029
|
+
pkgseer pkg quality <package> Get quality score
|
|
5030
|
+
pkgseer pkg deps <package> List dependencies
|
|
5031
|
+
pkgseer pkg compare <pkg...> Compare packages
|
|
5032
|
+
|
|
5033
|
+
Documentation commands:
|
|
5034
|
+
pkgseer docs list <package> List doc pages
|
|
5035
|
+
pkgseer docs get <pkg> <page> Fetch a doc page
|
|
5036
|
+
pkgseer docs search <query> Search documentation
|
|
1057
5037
|
|
|
1058
5038
|
Learn more at https://pkgseer.dev`);
|
|
5039
|
+
registerInitCommand(program);
|
|
1059
5040
|
registerMcpCommand(program);
|
|
1060
5041
|
registerLoginCommand(program);
|
|
1061
5042
|
registerLogoutCommand(program);
|
|
5043
|
+
registerQuickstartCommand(program);
|
|
1062
5044
|
var auth = program.command("auth").description("View and manage authentication");
|
|
1063
5045
|
registerAuthStatusCommand(auth);
|
|
5046
|
+
var config = program.command("config").description("View and manage configuration");
|
|
5047
|
+
registerConfigShowCommand(config);
|
|
5048
|
+
var pkg = program.command("pkg").description("Package intelligence commands");
|
|
5049
|
+
registerPkgInfoCommand(pkg);
|
|
5050
|
+
registerPkgQualityCommand(pkg);
|
|
5051
|
+
registerPkgDepsCommand(pkg);
|
|
5052
|
+
registerPkgVulnsCommand(pkg);
|
|
5053
|
+
registerPkgCompareCommand(pkg);
|
|
5054
|
+
var docs = program.command("docs").description("Package documentation commands");
|
|
5055
|
+
registerDocsListCommand(docs);
|
|
5056
|
+
registerDocsGetCommand(docs);
|
|
5057
|
+
registerDocsSearchCommand(docs);
|
|
5058
|
+
var project = program.command("project").description("Project management commands");
|
|
5059
|
+
registerProjectInitCommand(project);
|
|
5060
|
+
registerProjectDetectCommand(project);
|
|
5061
|
+
registerProjectUploadCommand(project);
|
|
1064
5062
|
program.parse();
|