@pkgseer/cli 0.1.0-alpha.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  version
4
- } from "./shared/chunk-awxns4wd.js";
4
+ } from "./shared/chunk-3ne7e7xh.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
  }
@@ -494,6 +898,32 @@ 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(authStorage, baseUrl) {
904
+ const envToken = process.env.PKGSEER_API_TOKEN;
905
+ if (envToken) {
906
+ return {
907
+ token: envToken,
908
+ tokenName: "PKGSEER_API_TOKEN",
909
+ scopes: [],
910
+ createdAt: new Date().toISOString(),
911
+ expiresAt: null,
912
+ apiKeyId: 0
913
+ };
914
+ }
915
+ const auth = await authStorage.load(baseUrl);
916
+ if (!auth) {
917
+ return null;
918
+ }
919
+ if (auth.expiresAt && new Date(auth.expiresAt) < new Date) {
920
+ return null;
921
+ }
922
+ if (!auth.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
923
+ return null;
924
+ }
925
+ return auth;
926
+ }
497
927
  // src/services/browser-service.ts
498
928
  import open from "open";
499
929
 
@@ -502,8 +932,143 @@ class BrowserServiceImpl {
502
932
  await open(url);
503
933
  }
504
934
  }
935
+ // src/services/config-service.ts
936
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
937
+ import { z } from "zod";
938
+ var CONFIG_DIR2 = ".pkgseer";
939
+ var GLOBAL_CONFIG_FILE = "config.yml";
940
+ var PROJECT_CONFIG_FILE = "pkgseer.yml";
941
+ var TOOL_NAMES = [
942
+ "package_summary",
943
+ "package_vulnerabilities",
944
+ "package_dependencies",
945
+ "package_quality",
946
+ "compare_packages",
947
+ "list_package_docs",
948
+ "fetch_package_doc",
949
+ "search_package_docs",
950
+ "search_project_docs"
951
+ ];
952
+ var ToolNameSchema = z.enum(TOOL_NAMES);
953
+ var SharedConfigFields = {
954
+ enabled_tools: z.array(ToolNameSchema).optional()
955
+ };
956
+ var GlobalConfigSchema = z.object({
957
+ ...SharedConfigFields
958
+ });
959
+ var ManifestGroupSchema = z.object({
960
+ label: z.string(),
961
+ files: z.array(z.string()),
962
+ allow_mix_deps: z.boolean().optional()
963
+ });
964
+ var ProjectConfigSchema = z.object({
965
+ ...SharedConfigFields,
966
+ project: z.string().optional(),
967
+ manifests: z.array(ManifestGroupSchema).optional()
968
+ });
969
+ var MergedConfigSchema = z.object({
970
+ enabled_tools: z.array(ToolNameSchema).optional(),
971
+ project: z.string().optional(),
972
+ manifests: z.array(ManifestGroupSchema).optional()
973
+ });
974
+
975
+ class ConfigServiceImpl {
976
+ fs;
977
+ constructor(fs) {
978
+ this.fs = fs;
979
+ }
980
+ getGlobalConfigPath() {
981
+ return this.fs.joinPath(this.fs.getHomeDir(), CONFIG_DIR2, GLOBAL_CONFIG_FILE);
982
+ }
983
+ async loadGlobalConfig() {
984
+ const configPath = this.getGlobalConfigPath();
985
+ return this.loadAndParseConfig(configPath, GlobalConfigSchema);
986
+ }
987
+ async loadProjectConfig() {
988
+ let currentDir = this.fs.getCwd();
989
+ while (true) {
990
+ const configPath = this.fs.joinPath(currentDir, PROJECT_CONFIG_FILE);
991
+ const config = await this.loadAndParseConfig(configPath, ProjectConfigSchema);
992
+ if (config) {
993
+ return { config, path: configPath };
994
+ }
995
+ const parentDir = this.fs.getDirname(currentDir);
996
+ if (parentDir === currentDir) {
997
+ break;
998
+ }
999
+ currentDir = parentDir;
1000
+ }
1001
+ return null;
1002
+ }
1003
+ async loadMergedConfig() {
1004
+ const [globalConfig, projectResult] = await Promise.all([
1005
+ this.loadGlobalConfig(),
1006
+ this.loadProjectConfig()
1007
+ ]);
1008
+ const merged = {};
1009
+ if (globalConfig?.enabled_tools) {
1010
+ merged.enabled_tools = globalConfig.enabled_tools;
1011
+ }
1012
+ if (projectResult?.config) {
1013
+ if (projectResult.config.enabled_tools) {
1014
+ merged.enabled_tools = projectResult.config.enabled_tools;
1015
+ }
1016
+ if (projectResult.config.project) {
1017
+ merged.project = projectResult.config.project;
1018
+ }
1019
+ }
1020
+ return {
1021
+ config: merged,
1022
+ globalPath: globalConfig ? this.getGlobalConfigPath() : null,
1023
+ projectPath: projectResult?.path ?? null
1024
+ };
1025
+ }
1026
+ async writeProjectConfig(config) {
1027
+ const validated = ProjectConfigSchema.parse(config);
1028
+ const currentDir = this.fs.getCwd();
1029
+ const configPath = this.fs.joinPath(currentDir, PROJECT_CONFIG_FILE);
1030
+ const existing = await this.loadProjectConfig();
1031
+ const existingConfig = existing?.config ?? {};
1032
+ const mergedConfig = {
1033
+ ...existingConfig,
1034
+ ...validated
1035
+ };
1036
+ const yamlContent = stringifyYaml(mergedConfig, {
1037
+ defaultStringType: "PLAIN",
1038
+ nullStr: ""
1039
+ });
1040
+ await this.fs.writeFile(configPath, yamlContent);
1041
+ }
1042
+ async loadAndParseConfig(path, schema) {
1043
+ const exists = await this.fs.exists(path);
1044
+ if (!exists) {
1045
+ return null;
1046
+ }
1047
+ try {
1048
+ const content = await this.fs.readFile(path);
1049
+ const parsed = parseYaml(content);
1050
+ if (parsed === null || parsed === undefined) {
1051
+ return schema.parse({});
1052
+ }
1053
+ const result = schema.safeParse(parsed);
1054
+ if (result.success) {
1055
+ return result.data;
1056
+ }
1057
+ return null;
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ }
1062
+ }
505
1063
  // src/services/filesystem-service.ts
506
- import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
1064
+ import {
1065
+ mkdir,
1066
+ readdir,
1067
+ readFile,
1068
+ stat,
1069
+ unlink,
1070
+ writeFile
1071
+ } from "node:fs/promises";
507
1072
  import { homedir } from "node:os";
508
1073
  import { dirname, join } from "node:path";
509
1074
 
@@ -540,6 +1105,50 @@ class FileSystemServiceImpl {
540
1105
  joinPath(...segments) {
541
1106
  return join(...segments);
542
1107
  }
1108
+ getCwd() {
1109
+ return process.cwd();
1110
+ }
1111
+ getDirname(path) {
1112
+ return dirname(path);
1113
+ }
1114
+ async readdir(path) {
1115
+ return readdir(path);
1116
+ }
1117
+ async isDirectory(path) {
1118
+ try {
1119
+ const stats = await stat(path);
1120
+ return stats.isDirectory();
1121
+ } catch {
1122
+ return false;
1123
+ }
1124
+ }
1125
+ }
1126
+ // src/services/git-service.ts
1127
+ import { exec } from "node:child_process";
1128
+ import { existsSync } from "node:fs";
1129
+ import { join as join2 } from "node:path";
1130
+ import { promisify } from "node:util";
1131
+ var execAsync = promisify(exec);
1132
+
1133
+ class GitServiceImpl {
1134
+ cwd;
1135
+ constructor(cwd = process.cwd()) {
1136
+ this.cwd = cwd;
1137
+ }
1138
+ async getCurrentBranch() {
1139
+ try {
1140
+ const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
1141
+ cwd: this.cwd
1142
+ });
1143
+ return stdout.trim() || null;
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+ async isGitRepository() {
1149
+ const gitPath = join2(this.cwd, ".git");
1150
+ return existsSync(gitPath);
1151
+ }
543
1152
  }
544
1153
  // src/services/pkgseer-service.ts
545
1154
  class PkgseerServiceImpl {
@@ -581,12 +1190,343 @@ class PkgseerServiceImpl {
581
1190
  const result = await this.client.ComparePackages({ packages });
582
1191
  return { data: result.data, errors: result.errors };
583
1192
  }
584
- }
585
- // src/container.ts
586
- async function resolveApiToken(authStorage, baseUrl) {
587
- const envToken = process.env.PKGSEER_API_TOKEN;
588
- if (envToken) {
589
- return envToken;
1193
+ async listPackageDocs(registry, packageName, version2) {
1194
+ const result = await this.client.ListPackageDocs({
1195
+ registry,
1196
+ packageName,
1197
+ version: version2
1198
+ });
1199
+ return { data: result.data, errors: result.errors };
1200
+ }
1201
+ async fetchPackageDoc(registry, packageName, pageId, version2) {
1202
+ const result = await this.client.FetchPackageDoc({
1203
+ registry,
1204
+ packageName,
1205
+ pageId,
1206
+ version: version2
1207
+ });
1208
+ return { data: result.data, errors: result.errors };
1209
+ }
1210
+ async searchPackageDocs(registry, packageName, options) {
1211
+ const result = await this.client.SearchPackageDocs({
1212
+ registry,
1213
+ packageName,
1214
+ keywords: options?.keywords,
1215
+ query: options?.query,
1216
+ includeSnippets: options?.includeSnippets,
1217
+ limit: options?.limit,
1218
+ version: options?.version
1219
+ });
1220
+ return { data: result.data, errors: result.errors };
1221
+ }
1222
+ async searchProjectDocs(project, options) {
1223
+ const result = await this.client.SearchProjectDocs({
1224
+ project,
1225
+ keywords: options?.keywords,
1226
+ query: options?.query,
1227
+ includeSnippets: options?.includeSnippets,
1228
+ limit: options?.limit
1229
+ });
1230
+ return { data: result.data, errors: result.errors };
1231
+ }
1232
+ async cliPackageInfo(registry, name) {
1233
+ const result = await this.client.CliPackageInfo({ registry, name });
1234
+ return { data: result.data, errors: result.errors };
1235
+ }
1236
+ async cliPackageVulns(registry, name, version2) {
1237
+ const result = await this.client.CliPackageVulns({
1238
+ registry,
1239
+ name,
1240
+ version: version2
1241
+ });
1242
+ return { data: result.data, errors: result.errors };
1243
+ }
1244
+ async cliPackageQuality(registry, name, version2) {
1245
+ const result = await this.client.CliPackageQuality({
1246
+ registry,
1247
+ name,
1248
+ version: version2
1249
+ });
1250
+ return { data: result.data, errors: result.errors };
1251
+ }
1252
+ async cliPackageDeps(registry, name, version2, includeTransitive) {
1253
+ const result = await this.client.CliPackageDeps({
1254
+ registry,
1255
+ name,
1256
+ version: version2,
1257
+ includeTransitive
1258
+ });
1259
+ return { data: result.data, errors: result.errors };
1260
+ }
1261
+ async cliComparePackages(packages) {
1262
+ const result = await this.client.CliComparePackages({ packages });
1263
+ return { data: result.data, errors: result.errors };
1264
+ }
1265
+ async cliDocsList(registry, packageName, version2) {
1266
+ const result = await this.client.CliDocsList({
1267
+ registry,
1268
+ packageName,
1269
+ version: version2
1270
+ });
1271
+ return { data: result.data, errors: result.errors };
1272
+ }
1273
+ async cliDocsGet(registry, packageName, pageId, version2) {
1274
+ const result = await this.client.CliDocsGet({
1275
+ registry,
1276
+ packageName,
1277
+ pageId,
1278
+ version: version2
1279
+ });
1280
+ return { data: result.data, errors: result.errors };
1281
+ }
1282
+ async cliDocsSearch(registry, packageName, options) {
1283
+ const result = await this.client.CliDocsSearch({
1284
+ registry,
1285
+ packageName,
1286
+ keywords: options?.keywords,
1287
+ query: options?.query,
1288
+ matchMode: options?.matchMode,
1289
+ limit: options?.limit,
1290
+ version: options?.version,
1291
+ contextLinesBefore: options?.contextLinesBefore,
1292
+ contextLinesAfter: options?.contextLinesAfter,
1293
+ maxMatches: options?.maxMatches
1294
+ });
1295
+ return { data: result.data, errors: result.errors };
1296
+ }
1297
+ async cliProjectDocsSearch(project, options) {
1298
+ const result = await this.client.CliProjectDocsSearch({
1299
+ project,
1300
+ keywords: options?.keywords,
1301
+ query: options?.query,
1302
+ matchMode: options?.matchMode,
1303
+ limit: options?.limit,
1304
+ contextLinesBefore: options?.contextLinesBefore,
1305
+ contextLinesAfter: options?.contextLinesAfter,
1306
+ maxMatches: options?.maxMatches
1307
+ });
1308
+ return { data: result.data, errors: result.errors };
1309
+ }
1310
+ }
1311
+ // src/services/project-service.ts
1312
+ var PROJECT_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1313
+ var MIN_NAME_LENGTH = 1;
1314
+ var MAX_NAME_LENGTH = 100;
1315
+ function validateProjectName(name) {
1316
+ const trimmedName = name?.trim() ?? "";
1317
+ if (!trimmedName || trimmedName.length === 0) {
1318
+ return { valid: false, error: "Project name cannot be empty" };
1319
+ }
1320
+ if (trimmedName.length < MIN_NAME_LENGTH) {
1321
+ return {
1322
+ valid: false,
1323
+ error: `Project name must be at least ${MIN_NAME_LENGTH} character`
1324
+ };
1325
+ }
1326
+ if (trimmedName.length > MAX_NAME_LENGTH) {
1327
+ return {
1328
+ valid: false,
1329
+ error: `Project name must be at most ${MAX_NAME_LENGTH} characters`
1330
+ };
1331
+ }
1332
+ if (!PROJECT_NAME_REGEX.test(trimmedName)) {
1333
+ return {
1334
+ valid: false,
1335
+ error: "Project name can only contain alphanumeric characters, hyphens, and underscores, and must start with an alphanumeric character"
1336
+ };
1337
+ }
1338
+ return { valid: true };
1339
+ }
1340
+
1341
+ class ProjectServiceImpl {
1342
+ client;
1343
+ baseUrl;
1344
+ apiToken;
1345
+ constructor(client, baseUrl, apiToken) {
1346
+ this.client = client;
1347
+ this.baseUrl = baseUrl;
1348
+ this.apiToken = apiToken;
1349
+ }
1350
+ async createProject(input) {
1351
+ const validation = validateProjectName(input.name);
1352
+ if (!validation.valid) {
1353
+ throw new Error(validation.error);
1354
+ }
1355
+ const result = await this.client.CreateProject({ input });
1356
+ if (result.errors && result.errors.length > 0 && result.errors[0]) {
1357
+ throw new Error(result.errors[0].message);
1358
+ }
1359
+ if (!result.data?.createProject) {
1360
+ throw new Error("Failed to create project");
1361
+ }
1362
+ if (result.data.createProject.errors && result.data.createProject.errors.length > 0) {
1363
+ const firstError = result.data.createProject.errors[0];
1364
+ if (firstError) {
1365
+ throw new Error(firstError.message ?? "Validation failed");
1366
+ }
1367
+ }
1368
+ return {
1369
+ project: result.data.createProject.project ? {
1370
+ name: result.data.createProject.project.name,
1371
+ defaultBranch: result.data.createProject.project.defaultBranch
1372
+ } : null,
1373
+ errors: result.data.createProject.errors?.map((e) => ({
1374
+ field: e?.field ?? "",
1375
+ message: e?.message ?? ""
1376
+ })) ?? null
1377
+ };
1378
+ }
1379
+ detectPairedManifests(files) {
1380
+ if (files.length !== 2) {
1381
+ return null;
1382
+ }
1383
+ const depsTreeFile = files.find((f) => f.filename === "deps-tree.txt");
1384
+ const depsFile = files.find((f) => f.filename === "deps.txt");
1385
+ if (depsTreeFile && depsFile) {
1386
+ return { projectFile: depsTreeFile, lockFile: depsFile };
1387
+ }
1388
+ const pyprojectFile = files.find((f) => f.filename === "pyproject.toml");
1389
+ const poetryLockFile = files.find((f) => f.filename === "poetry.lock");
1390
+ if (pyprojectFile && poetryLockFile) {
1391
+ return { projectFile: pyprojectFile, lockFile: poetryLockFile };
1392
+ }
1393
+ const pipfile = files.find((f) => f.filename === "Pipfile");
1394
+ const pipfileLock = files.find((f) => f.filename === "Pipfile.lock");
1395
+ if (pipfile && pipfileLock) {
1396
+ return { projectFile: pipfile, lockFile: pipfileLock };
1397
+ }
1398
+ return null;
1399
+ }
1400
+ async uploadManifests(params) {
1401
+ const { project, branch, label, files } = params;
1402
+ if (files.length === 0) {
1403
+ throw new Error("At least one file is required");
1404
+ }
1405
+ const formData = new FormData;
1406
+ formData.append("branch", branch);
1407
+ formData.append("label", label);
1408
+ const pairedManifests = this.detectPairedManifests(files);
1409
+ if (pairedManifests) {
1410
+ const { projectFile, lockFile } = pairedManifests;
1411
+ for (const file of [projectFile, lockFile]) {
1412
+ const sizeInBytes = new TextEncoder().encode(file.content).length;
1413
+ const sizeInMB = sizeInBytes / (1024 * 1024);
1414
+ if (sizeInMB > 10) {
1415
+ throw new Error(`File ${file.filename} exceeds 10MB limit (${sizeInMB.toFixed(2)}MB)`);
1416
+ }
1417
+ }
1418
+ const projectBlob = new Blob([projectFile.content], {
1419
+ type: "text/plain"
1420
+ });
1421
+ const lockBlob = new Blob([lockFile.content], { type: "text/plain" });
1422
+ formData.append("project_file", projectBlob, projectFile.filename);
1423
+ formData.append("lock_file", lockBlob, lockFile.filename);
1424
+ } else {
1425
+ for (const file of files) {
1426
+ const sizeInBytes = new TextEncoder().encode(file.content).length;
1427
+ const sizeInMB = sizeInBytes / (1024 * 1024);
1428
+ if (sizeInMB > 10) {
1429
+ throw new Error(`File ${file.filename} exceeds 10MB limit (${sizeInMB.toFixed(2)}MB)`);
1430
+ }
1431
+ const blob = new Blob([file.content], { type: "text/plain" });
1432
+ formData.append("files[]", blob, file.filename);
1433
+ }
1434
+ }
1435
+ const url = `${this.baseUrl}/api/projects/${project}/manifests`;
1436
+ const response = await fetch(url, {
1437
+ method: "POST",
1438
+ headers: {
1439
+ Authorization: `Bearer ${this.apiToken}`
1440
+ },
1441
+ body: formData
1442
+ });
1443
+ if (!response.ok) {
1444
+ let errorMessage = `Failed to upload manifests: ${response.status} ${response.statusText}`;
1445
+ try {
1446
+ const responseClone = response.clone();
1447
+ const errorData = await responseClone.json();
1448
+ if (errorData.error?.message) {
1449
+ errorMessage = errorData.error.message;
1450
+ } else if (errorData.error?.code) {
1451
+ errorMessage = `Upload failed: ${errorData.error.code}`;
1452
+ }
1453
+ } catch {
1454
+ try {
1455
+ const responseClone = response.clone();
1456
+ const errorText = await responseClone.text();
1457
+ if (errorText) {
1458
+ errorMessage = `Failed to upload manifests: ${errorText}`;
1459
+ }
1460
+ } catch {
1461
+ errorMessage = `Failed to upload manifests: ${response.status} ${response.statusText}`;
1462
+ }
1463
+ }
1464
+ if (response.status === 401) {
1465
+ throw new Error("Authentication required. Please run `pkgseer login`.");
1466
+ }
1467
+ if (response.status === 403) {
1468
+ throw new Error("Insufficient permissions. Please run `pkgseer login` with proper scopes.");
1469
+ }
1470
+ if (response.status === 404) {
1471
+ throw new Error(`Project not found: ${project}`);
1472
+ }
1473
+ if (response.status === 429) {
1474
+ const retryAfter = response.headers.get("Retry-After");
1475
+ const retryMsg = retryAfter ? ` Please retry after ${retryAfter} seconds.` : "";
1476
+ throw new Error(`Upload rate limit exceeded.${retryMsg}`);
1477
+ }
1478
+ throw new Error(errorMessage);
1479
+ }
1480
+ return response.json();
1481
+ }
1482
+ }
1483
+ // src/services/prompt-service.ts
1484
+ import { confirm, input, select } from "@inquirer/prompts";
1485
+
1486
+ class PromptServiceImpl {
1487
+ async input(message, defaultValue) {
1488
+ return input({ message, default: defaultValue });
1489
+ }
1490
+ async select(message, options) {
1491
+ return select({
1492
+ message,
1493
+ choices: options.map((opt) => ({
1494
+ value: opt.value,
1495
+ name: opt.name,
1496
+ description: opt.description
1497
+ }))
1498
+ });
1499
+ }
1500
+ async confirm(message, defaultValue) {
1501
+ return confirm({ message, default: defaultValue });
1502
+ }
1503
+ }
1504
+ // src/services/shell-service.ts
1505
+ import { exec as exec2 } from "node:child_process";
1506
+ import { promisify as promisify2 } from "node:util";
1507
+ var execAsync2 = promisify2(exec2);
1508
+
1509
+ class ShellServiceImpl {
1510
+ async execute(command, cwd) {
1511
+ try {
1512
+ const { stdout } = await execAsync2(command, {
1513
+ cwd,
1514
+ maxBuffer: 10 * 1024 * 1024
1515
+ });
1516
+ return stdout.trim();
1517
+ } catch (error) {
1518
+ const errorMessage = error instanceof Error ? error.message : String(error);
1519
+ throw new Error(`Command failed: ${command}
1520
+ Working directory: ${cwd}
1521
+ Error: ${errorMessage}`);
1522
+ }
1523
+ }
1524
+ }
1525
+ // src/container.ts
1526
+ async function resolveApiToken(authStorage, baseUrl) {
1527
+ const envToken = process.env.PKGSEER_API_TOKEN;
1528
+ if (envToken) {
1529
+ return envToken;
590
1530
  }
591
1531
  const stored = await authStorage.load(baseUrl);
592
1532
  if (stored) {
@@ -601,7 +1541,11 @@ async function createContainer() {
601
1541
  const baseUrl = getBaseUrl();
602
1542
  const fileSystemService = new FileSystemServiceImpl;
603
1543
  const authStorage = new AuthStorageImpl(fileSystemService);
604
- const apiToken = await resolveApiToken(authStorage, baseUrl);
1544
+ const configService = new ConfigServiceImpl(fileSystemService);
1545
+ const [apiToken, configResult] = await Promise.all([
1546
+ resolveApiToken(authStorage, baseUrl),
1547
+ configService.loadMergedConfig()
1548
+ ]);
605
1549
  const client = createClient(apiToken);
606
1550
  return {
607
1551
  pkgseerService: new PkgseerServiceImpl(client),
@@ -609,7 +1553,14 @@ async function createContainer() {
609
1553
  authService: new AuthServiceImpl(baseUrl),
610
1554
  browserService: new BrowserServiceImpl,
611
1555
  fileSystemService,
1556
+ configService,
1557
+ projectService: new ProjectServiceImpl(client, baseUrl, apiToken ?? ""),
1558
+ gitService: new GitServiceImpl,
1559
+ promptService: new PromptServiceImpl,
1560
+ shellService: new ShellServiceImpl,
1561
+ config: configResult.config,
612
1562
  baseUrl,
1563
+ apiToken,
613
1564
  hasValidToken: apiToken !== undefined
614
1565
  };
615
1566
  }
@@ -663,388 +1614,2895 @@ function registerAuthStatusCommand(program) {
663
1614
  });
664
1615
  }
665
1616
 
666
- // src/commands/login.ts
667
- import { hostname } from "node:os";
668
- var TIMEOUT_MS = 5 * 60 * 1000;
669
- function randomPort() {
670
- return Math.floor(Math.random() * 2000) + 8000;
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...
1617
+ // src/commands/config-show.ts
1618
+ async function configShowAction(deps) {
1619
+ const { configService } = deps;
1620
+ const { config, globalPath, projectPath } = await configService.loadMergedConfig();
1621
+ if (!globalPath && !projectPath) {
1622
+ console.log(`No configuration found.
687
1623
  `);
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:
1624
+ console.log(" Global config: ~/.pkgseer/config.yml");
1625
+ console.log(` Project config: pkgseer.yml
700
1626
  `);
701
- console.log(` ${authUrl}
702
- `);
703
- } else {
704
- console.log("Opening browser...");
705
- await browserService.open(authUrl);
1627
+ console.log("Create a config file to customize pkgseer behavior.");
1628
+ return;
706
1629
  }
707
- console.log(`Waiting for authentication...
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}.
1630
+ console.log(`Configuration sources:
721
1631
  `);
722
- console.log("Run `pkgseer login` to try again.");
723
- }
724
- process.exit(1);
1632
+ if (globalPath) {
1633
+ console.log(` Global: ${globalPath}`);
725
1634
  }
726
- if (callback.state !== state) {
727
- console.error(`Security error: authentication state mismatch.
728
- `);
729
- console.log("This could indicate a security issue. Please try again.");
730
- process.exit(1);
1635
+ if (projectPath) {
1636
+ console.log(` Project: ${projectPath}`);
731
1637
  }
732
- let tokenResponse;
733
- try {
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}
1638
+ console.log(`
1639
+ Merged configuration:
741
1640
  `);
742
- console.log("Run `pkgseer login` to try again.");
743
- process.exit(1);
1641
+ if (config.enabled_tools !== undefined) {
1642
+ if (config.enabled_tools.length > 0) {
1643
+ console.log(" enabled_tools:");
1644
+ for (const tool of config.enabled_tools) {
1645
+ console.log(` - ${tool}`);
1646
+ }
1647
+ } else {
1648
+ console.log(" enabled_tools: [] (no tools enabled)");
1649
+ }
1650
+ } else {
1651
+ console.log(" enabled_tools: (all tools enabled by default)");
744
1652
  }
745
- await authStorage.save(baseUrl, {
746
- token: tokenResponse.token,
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`);
1653
+ if (config.project) {
1654
+ console.log(` project: ${config.project}`);
760
1655
  }
761
- console.log(`
762
- You're ready to use pkgseer with your AI assistant.`);
763
1656
  }
764
- var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
765
-
766
- Opens your browser to complete authentication securely. The CLI receives
767
- a token that's stored locally and used for API requests.
1657
+ var SHOW_DESCRIPTION = `Display current configuration.
768
1658
 
769
- Use --no-browser in environments without a display (CI, SSH sessions)
770
- to get a URL you can open on another device.`;
771
- function registerLoginCommand(program) {
772
- 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).action(async (options) => {
1659
+ Shows the merged configuration from global (~/.pkgseer/config.yml) and
1660
+ project (pkgseer.yml) config files. Project config takes precedence
1661
+ over global config for overlapping settings.`;
1662
+ function registerConfigShowCommand(program) {
1663
+ program.command("show").summary("Display current configuration").description(SHOW_DESCRIPTION).action(async () => {
773
1664
  const deps = await createContainer();
774
- await loginAction(options, deps);
1665
+ await configShowAction(deps);
775
1666
  });
776
1667
  }
777
1668
 
778
- // src/commands/logout.ts
779
- async function logoutAction(deps) {
780
- const { authService, authStorage, baseUrl } = deps;
781
- const auth = await authStorage.load(baseUrl);
782
- if (!auth) {
783
- console.log(`Not currently logged in.
784
- `);
785
- console.log(` Environment: ${baseUrl}`);
786
- return;
1669
+ // src/commands/shared.ts
1670
+ function toGraphQLRegistry(registry) {
1671
+ const map = {
1672
+ npm: "NPM",
1673
+ pypi: "PYPI",
1674
+ hex: "HEX"
1675
+ };
1676
+ return map[registry.toLowerCase()] || "NPM";
1677
+ }
1678
+ function output(data, json) {
1679
+ if (json) {
1680
+ console.log(JSON.stringify(data));
1681
+ } else {
1682
+ console.log(data);
787
1683
  }
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
1684
  }
796
- var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
797
-
798
- Clears the locally stored authentication token and notifies the server
799
- to revoke it. Use this when switching accounts or on shared machines.`;
800
- function registerLogoutCommand(program) {
801
- program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
1685
+ function outputError(message, json) {
1686
+ if (json) {
1687
+ console.error(JSON.stringify({ error: message }));
1688
+ } else {
1689
+ console.error(`Error: ${message}`);
1690
+ }
1691
+ process.exit(1);
1692
+ }
1693
+ function handleErrors(errors, json) {
1694
+ if (errors && errors.length > 0) {
1695
+ const message = errors.map((e) => e.message).join(", ");
1696
+ outputError(message, json);
1697
+ }
1698
+ }
1699
+ function formatNumber(num) {
1700
+ if (num == null)
1701
+ return "N/A";
1702
+ return num.toLocaleString();
1703
+ }
1704
+ function keyValueTable(pairs) {
1705
+ const maxKeyLen = Math.max(...pairs.map(([k]) => k.length));
1706
+ return pairs.map(([key, value]) => ` ${key.padEnd(maxKeyLen)} ${value ?? "N/A"}`).join(`
1707
+ `);
1708
+ }
1709
+ function formatSeverity(severity) {
1710
+ const indicators = {
1711
+ CRITICAL: "\uD83D\uDD34 CRITICAL",
1712
+ HIGH: "\uD83D\uDFE0 HIGH",
1713
+ MEDIUM: "\uD83D\uDFE1 MEDIUM",
1714
+ LOW: "\uD83D\uDFE2 LOW",
1715
+ UNKNOWN: "⚪ UNKNOWN"
1716
+ };
1717
+ return indicators[severity] || severity;
1718
+ }
1719
+ function extractGraphQLError(error) {
1720
+ if (error && typeof error === "object" && "response" in error && error.response && typeof error.response === "object" && "errors" in error.response && Array.isArray(error.response.errors)) {
1721
+ const errors = error.response.errors;
1722
+ if (errors.length > 0) {
1723
+ const messages = errors.map((e) => e.message).filter((m) => typeof m === "string");
1724
+ if (messages.length > 0) {
1725
+ return messages.join("; ");
1726
+ }
1727
+ }
1728
+ }
1729
+ if (error && typeof error === "object" && "errors" in error && Array.isArray(error.errors)) {
1730
+ const errors = error.errors;
1731
+ if (errors.length > 0) {
1732
+ const messages = errors.map((e) => e.message).filter((m) => typeof m === "string");
1733
+ if (messages.length > 0) {
1734
+ return messages.join("; ");
1735
+ }
1736
+ }
1737
+ }
1738
+ if (error instanceof Error) {
1739
+ const message = error.message;
1740
+ const jsonMatch = message.match(/"message"\s*:\s*"([^"]+)"/);
1741
+ if (jsonMatch?.[1]) {
1742
+ return jsonMatch[1];
1743
+ }
1744
+ if (message.includes("Cannot query field") || message.includes("Unknown field")) {
1745
+ return message;
1746
+ }
1747
+ }
1748
+ return null;
1749
+ }
1750
+ function formatErrorMessage(errorMessage, isJson) {
1751
+ if (isJson) {
1752
+ return errorMessage;
1753
+ }
1754
+ if (errorMessage.includes("Cannot query field") || errorMessage.includes("Unknown field") || errorMessage.includes("Field") && errorMessage.includes("doesn't exist")) {
1755
+ return `${errorMessage}
1756
+
1757
+ ` + `This error usually indicates a schema mismatch. Possible causes:
1758
+ ` + ` - The server schema is outdated (try updating the server)
1759
+ ` + ` - The client schema is outdated (run: bun run codegen)
1760
+ ` + ` - You're querying a different server than expected (check PKGSEER_URL)`;
1761
+ }
1762
+ return errorMessage;
1763
+ }
1764
+ async function withCliErrorHandling(json, fn) {
1765
+ try {
1766
+ await fn();
1767
+ } catch (error) {
1768
+ const graphQLError = extractGraphQLError(error);
1769
+ if (graphQLError) {
1770
+ const formatted = formatErrorMessage(graphQLError, json);
1771
+ outputError(formatted, json);
1772
+ return;
1773
+ }
1774
+ const message = error instanceof Error ? error.message : "Unknown error";
1775
+ const colonMatch = message.match(/^([^:]+):/);
1776
+ const shortMessage = colonMatch?.[1] ?? message;
1777
+ outputError(shortMessage, json);
1778
+ }
1779
+ }
1780
+ function formatScore(score) {
1781
+ if (score == null)
1782
+ return "N/A";
1783
+ const percentage = score > 1 ? Math.round(score) : Math.round(score * 100);
1784
+ const filled = Math.round(percentage / 5);
1785
+ const bar = "█".repeat(Math.min(filled, 20)) + "░".repeat(Math.max(20 - filled, 0));
1786
+ return `${bar} ${percentage}%`;
1787
+ }
1788
+
1789
+ // src/commands/docs/get.ts
1790
+ function parsePackageRef(ref) {
1791
+ if (!ref.includes("/")) {
1792
+ throw new Error(`Invalid format: "${ref}". Expected format:
1793
+ ` + ` Full form: <registry>/<package>/<version>/<document>
1794
+ ` + ` Short form: <package>/<document>
1795
+ ` + `Examples: npm/express/4.18.2/readme or express/readme`);
1796
+ }
1797
+ const parts = ref.split("/");
1798
+ if (parts.length < 2) {
1799
+ throw new Error(`Invalid format: "${ref}". Expected at least 2 parts separated by "/"`);
1800
+ }
1801
+ if (parts.length >= 4) {
1802
+ const registry = parts[0];
1803
+ const packageName2 = parts[1];
1804
+ const version2 = parts[2];
1805
+ const pageId2 = parts.slice(3).join("/");
1806
+ if (!registry || !packageName2 || !version2 || !pageId2) {
1807
+ throw new Error(`Invalid full form: "${ref}". All components (registry/package/version/document) are required.`);
1808
+ }
1809
+ return {
1810
+ registry: registry.toLowerCase(),
1811
+ packageName: packageName2,
1812
+ version: version2,
1813
+ pageId: pageId2,
1814
+ originalRef: ref
1815
+ };
1816
+ }
1817
+ const packageName = parts[0];
1818
+ const pageId = parts.slice(1).join("/");
1819
+ if (!packageName || !pageId) {
1820
+ throw new Error(`Invalid format: "${ref}". Expected format: <package>/<document>`);
1821
+ }
1822
+ return { packageName, pageId, originalRef: ref };
1823
+ }
1824
+ function formatDocPage(data, ref) {
1825
+ const lines = [];
1826
+ const page = data.page;
1827
+ if (!page) {
1828
+ return `[${ref}] No page content available.`;
1829
+ }
1830
+ lines.push(`[${ref}]`);
1831
+ lines.push("");
1832
+ if (page.breadcrumbs && page.breadcrumbs.length > 0) {
1833
+ lines.push(page.breadcrumbs.join(" > "));
1834
+ lines.push("");
1835
+ }
1836
+ lines.push(`# ${page.title}`);
1837
+ lines.push("");
1838
+ if (page.content) {
1839
+ lines.push(page.content);
1840
+ } else {
1841
+ lines.push("(No content available)");
1842
+ }
1843
+ return lines.join(`
1844
+ `);
1845
+ }
1846
+ function extractErrorMessage(error) {
1847
+ if (error instanceof Error) {
1848
+ const message = error.message;
1849
+ if (message.includes('"response"')) {
1850
+ const colonIndex = message.indexOf(":");
1851
+ if (colonIndex > 0) {
1852
+ return message.slice(0, colonIndex).trim();
1853
+ }
1854
+ }
1855
+ return message;
1856
+ }
1857
+ return "Unknown error occurred";
1858
+ }
1859
+ async function fetchPage(ref, defaultRegistry, defaultVersion, pkgseerService) {
1860
+ try {
1861
+ const registry = ref.registry ? toGraphQLRegistry(ref.registry) : defaultRegistry;
1862
+ const version2 = ref.version ?? defaultVersion;
1863
+ const result = await pkgseerService.cliDocsGet(registry, ref.packageName, ref.pageId, version2);
1864
+ if (result.errors && result.errors.length > 0) {
1865
+ const errorMsg = result.errors.map((e) => {
1866
+ if (typeof e === "object" && e !== null && "message" in e) {
1867
+ return String(e.message);
1868
+ }
1869
+ return String(e);
1870
+ }).join("; ");
1871
+ return { ref: ref.originalRef, result: null, error: errorMsg };
1872
+ }
1873
+ if (!result.data.fetchPackageDoc) {
1874
+ return {
1875
+ ref: ref.originalRef,
1876
+ result: null,
1877
+ error: `Page not found: ${ref.pageId} for ${ref.packageName}`
1878
+ };
1879
+ }
1880
+ return { ref: ref.originalRef, result: result.data.fetchPackageDoc };
1881
+ } catch (error) {
1882
+ return {
1883
+ ref: ref.originalRef,
1884
+ result: null,
1885
+ error: extractErrorMessage(error)
1886
+ };
1887
+ }
1888
+ }
1889
+ async function docsGetAction(refs, options, deps) {
1890
+ if (refs.length === 0) {
1891
+ outputError("At least one page reference required. Format: <package>/<slug>", options.json ?? false);
1892
+ return;
1893
+ }
1894
+ const { pkgseerService } = deps;
1895
+ const defaultRegistry = toGraphQLRegistry(options.registry);
1896
+ const parsedRefs = [];
1897
+ for (const ref of refs) {
1898
+ try {
1899
+ parsedRefs.push(parsePackageRef(ref));
1900
+ } catch (error) {
1901
+ const message = error instanceof Error ? error.message : "Unknown error";
1902
+ outputError(`Invalid reference "${ref}": ${message}`, options.json ?? false);
1903
+ return;
1904
+ }
1905
+ }
1906
+ const results = await Promise.all(parsedRefs.map((ref) => fetchPage(ref, defaultRegistry, options.pkgVersion, pkgseerService)));
1907
+ const errors = results.filter((r) => r.error);
1908
+ const successes = results.filter((r) => r.result);
1909
+ if (errors.length === results.length) {
1910
+ const errorMessages = errors.map((e) => ` ${e.ref}: ${e.error}`).join(`
1911
+ `);
1912
+ outputError(`Failed to fetch all pages:
1913
+ ${errorMessages}`, options.json ?? false);
1914
+ return;
1915
+ }
1916
+ if (errors.length > 0 && !options.json) {
1917
+ for (const { ref, error } of errors) {
1918
+ console.error(`Error fetching ${ref}: ${error}`);
1919
+ }
1920
+ if (successes.length > 0) {
1921
+ console.error("");
1922
+ }
1923
+ }
1924
+ if (options.json) {
1925
+ const jsonResults = results.map((r) => {
1926
+ if (r.error) {
1927
+ return { ref: r.ref, error: r.error };
1928
+ }
1929
+ const page = r.result?.page;
1930
+ return {
1931
+ ref: r.ref,
1932
+ title: page?.title,
1933
+ content: page?.content
1934
+ };
1935
+ });
1936
+ output(jsonResults, true);
1937
+ } else {
1938
+ if (successes.length > 0) {
1939
+ const pages = successes.filter((r) => r.result !== null).map((r) => formatDocPage(r.result, r.ref));
1940
+ console.log(pages.join(`
1941
+
1942
+ ---
1943
+
1944
+ `));
1945
+ }
1946
+ }
1947
+ if (errors.length > 0) {
1948
+ process.exit(1);
1949
+ }
1950
+ }
1951
+ var GET_DESCRIPTION = `Fetch one or more documentation pages.
1952
+
1953
+ Retrieves the full content of documentation pages including
1954
+ title, breadcrumbs, content (markdown), and source URL.
1955
+
1956
+ Use 'pkgseer docs list' first to discover available page IDs, or
1957
+ use the output format from 'pkgseer docs search --refs-only'.
1958
+
1959
+ Format: <registry>/<package>/<version>/<document> (full form)
1960
+ or <package>/<document> (short form, requires --registry/--pkg-version)
1961
+
1962
+ Examples:
1963
+ # Full form (from search output)
1964
+ pkgseer docs get npm/express/4.18.2/readme
1965
+ pkgseer docs get hex/postgrex/1.15.0/readme
1966
+
1967
+ # Short form (backward compatibility)
1968
+ pkgseer docs get express/readme --registry npm --pkg-version 4.18.2
1969
+ pkgseer docs get postgrex/readme --registry hex
1970
+
1971
+ # Multiple pages
1972
+ pkgseer docs get npm/express/4.18.2/readme npm/express/4.18.2/writing-middleware
1973
+
1974
+ # Pipe from search (full form works seamlessly)
1975
+ pkgseer docs search log --package express --refs-only | \\
1976
+ xargs pkgseer docs get`;
1977
+ function registerDocsGetCommand(program) {
1978
+ 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) => {
1979
+ await withCliErrorHandling(options.json ?? false, async () => {
1980
+ const deps = await createContainer();
1981
+ await docsGetAction(refs, options, deps);
1982
+ });
1983
+ });
1984
+ }
1985
+ // src/commands/docs/list.ts
1986
+ function formatDocsList(docs) {
1987
+ const lines = [];
1988
+ lines.push(`\uD83D\uDCDA Documentation: ${docs.packageName}@${docs.version}`);
1989
+ lines.push("");
1990
+ if (!docs.pages || docs.pages.length === 0) {
1991
+ lines.push("No documentation pages found.");
1992
+ return lines.join(`
1993
+ `);
1994
+ }
1995
+ lines.push(`Found ${docs.pages.length} pages:`);
1996
+ lines.push("");
1997
+ for (const page of docs.pages) {
1998
+ if (!page)
1999
+ continue;
2000
+ const words = page.words ? ` (${formatNumber(page.words)} words)` : "";
2001
+ lines.push(` ${page.title}${words}`);
2002
+ lines.push(` ID: ${page.slug}`);
2003
+ lines.push("");
2004
+ }
2005
+ lines.push("Use 'pkgseer docs get <package>/<page-id>' to fetch a page.");
2006
+ return lines.join(`
2007
+ `);
2008
+ }
2009
+ async function docsListAction(packageName, options, deps) {
2010
+ const { pkgseerService } = deps;
2011
+ const registry = toGraphQLRegistry(options.registry);
2012
+ const result = await pkgseerService.cliDocsList(registry, packageName, options.pkgVersion);
2013
+ handleErrors(result.errors, options.json ?? false);
2014
+ if (!result.data.listPackageDocs) {
2015
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
2016
+ return;
2017
+ }
2018
+ if (options.json) {
2019
+ const pages = result.data.listPackageDocs.pages?.filter((p) => p) ?? [];
2020
+ const slim = pages.map((p) => ({
2021
+ slug: p.slug,
2022
+ title: p.title
2023
+ }));
2024
+ output(slim, true);
2025
+ } else {
2026
+ console.log(formatDocsList(result.data.listPackageDocs));
2027
+ }
2028
+ }
2029
+ var LIST_DESCRIPTION = `List available documentation pages for a package.
2030
+
2031
+ Shows all documentation pages with titles, page IDs (slugs),
2032
+ word counts, and descriptions. Use the page ID with 'docs get'
2033
+ to fetch the full content of a specific page.
2034
+
2035
+ Examples:
2036
+ pkgseer docs list lodash
2037
+ pkgseer docs list requests --registry pypi
2038
+ pkgseer docs list phoenix --registry hex --version 1.7.0 --json`;
2039
+ function registerDocsListCommand(program) {
2040
+ 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) => {
2041
+ await withCliErrorHandling(options.json ?? false, async () => {
2042
+ const deps = await createContainer();
2043
+ await docsListAction(packageName, options, deps);
2044
+ });
2045
+ });
2046
+ }
2047
+ // src/commands/docs/search.ts
2048
+ function buildDocRef(entry, searchResult) {
2049
+ if (!entry?.slug)
2050
+ return "";
2051
+ let registry;
2052
+ let packageName;
2053
+ let version2;
2054
+ if ("packageName" in entry && "registry" in entry && "version" in entry) {
2055
+ registry = entry.registry;
2056
+ packageName = entry.packageName;
2057
+ version2 = entry.version;
2058
+ } else {
2059
+ const rawRegistry = "registry" in searchResult ? searchResult.registry : null;
2060
+ registry = rawRegistry ? rawRegistry.toLowerCase() : null;
2061
+ packageName = "packageName" in searchResult ? searchResult.packageName : null;
2062
+ version2 = "version" in searchResult ? searchResult.version : null;
2063
+ }
2064
+ const parts = [];
2065
+ if (registry)
2066
+ parts.push(registry.toLowerCase());
2067
+ if (packageName)
2068
+ parts.push(packageName);
2069
+ if (version2)
2070
+ parts.push(version2);
2071
+ else if (registry && packageName)
2072
+ parts.push("latest");
2073
+ if (entry.slug)
2074
+ parts.push(entry.slug);
2075
+ return parts.join("/");
2076
+ }
2077
+ var colors = {
2078
+ reset: "\x1B[0m",
2079
+ bold: "\x1B[1m",
2080
+ dim: "\x1B[2m",
2081
+ magenta: "\x1B[35m",
2082
+ cyan: "\x1B[36m",
2083
+ yellow: "\x1B[33m",
2084
+ green: "\x1B[32m"
2085
+ };
2086
+ function shouldUseColors(noColor) {
2087
+ if (noColor)
2088
+ return false;
2089
+ if (process.env.NO_COLOR !== undefined)
2090
+ return false;
2091
+ return process.stdout.isTTY ?? false;
2092
+ }
2093
+ function applyHighlights(line, highlights, useColors) {
2094
+ if (!highlights || highlights.length === 0 || !useColors) {
2095
+ return line;
2096
+ }
2097
+ const sorted = [...highlights].filter((h) => h && h.start != null && h.end != null).sort((a, b) => {
2098
+ const aStart = a?.start ?? 0;
2099
+ const bStart = b?.start ?? 0;
2100
+ return bStart - aStart;
2101
+ });
2102
+ let result = line;
2103
+ for (const h of sorted) {
2104
+ if (!h || h.start == null || h.end == null)
2105
+ continue;
2106
+ const before = result.slice(0, h.start);
2107
+ const match = result.slice(h.start, h.end);
2108
+ const after = result.slice(h.end);
2109
+ result = `${before}${colors.bold}${colors.magenta}${match}${colors.reset}${after}`;
2110
+ }
2111
+ return result;
2112
+ }
2113
+ function formatEntry(entry, searchResult, useColors) {
2114
+ if (!entry)
2115
+ return "";
2116
+ const lines = [];
2117
+ const ref = buildDocRef(entry, searchResult);
2118
+ const matchInfo = entry.matchCount && entry.matchCount > 0 ? ` (${entry.matchCount} match${entry.matchCount > 1 ? "es" : ""})` : "";
2119
+ if (useColors) {
2120
+ lines.push(`${colors.cyan}${ref}${colors.reset}: ${colors.bold}${entry.title}${colors.reset}${colors.dim}${matchInfo}${colors.reset}`);
2121
+ } else {
2122
+ lines.push(`${ref}: ${entry.title}${matchInfo}`);
2123
+ }
2124
+ const matches = entry.matches ?? [];
2125
+ for (const match of matches) {
2126
+ if (!match?.context)
2127
+ continue;
2128
+ const ctx = match.context;
2129
+ for (const beforeLine of ctx.before ?? []) {
2130
+ if (beforeLine) {
2131
+ const prefix = useColors ? colors.dim : "";
2132
+ const suffix = useColors ? colors.reset : "";
2133
+ lines.push(` ${prefix}${beforeLine}${suffix}`);
2134
+ }
2135
+ }
2136
+ for (const matchedLine of ctx.matchedLines ?? []) {
2137
+ if (matchedLine?.content) {
2138
+ const highlighted = applyHighlights(matchedLine.content, matchedLine.highlights, useColors);
2139
+ lines.push(` ${highlighted}`);
2140
+ }
2141
+ }
2142
+ for (const afterLine of ctx.after ?? []) {
2143
+ if (afterLine) {
2144
+ const prefix = useColors ? colors.dim : "";
2145
+ const suffix = useColors ? colors.reset : "";
2146
+ lines.push(` ${prefix}${afterLine}${suffix}`);
2147
+ }
2148
+ }
2149
+ lines.push("");
2150
+ }
2151
+ return lines.join(`
2152
+ `);
2153
+ }
2154
+ function formatSearchResults(results, useColors) {
2155
+ const entries = results.entries ?? [];
2156
+ if (entries.length === 0) {
2157
+ return "No matching documentation found.";
2158
+ }
2159
+ const formatted = entries.filter((e) => e != null).map((entry) => formatEntry(entry, results, useColors));
2160
+ return formatted.join(`
2161
+ `);
2162
+ }
2163
+ function formatRefsOnly(results) {
2164
+ const entries = results.entries ?? [];
2165
+ return entries.filter((e) => e != null).map((entry) => buildDocRef(entry, results)).join(`
2166
+ `);
2167
+ }
2168
+ function formatCount(results) {
2169
+ const entries = results.entries ?? [];
2170
+ return entries.filter((e) => e != null).map((entry) => {
2171
+ if (!entry)
2172
+ return "";
2173
+ const ref = buildDocRef(entry, results);
2174
+ return `${ref}: ${entry.matchCount ?? 0}`;
2175
+ }).filter((s) => s !== "").join(`
2176
+ `);
2177
+ }
2178
+ function slimSearchResults(results) {
2179
+ const entries = results.entries ?? [];
2180
+ return entries.filter((e) => e != null).map((entry) => {
2181
+ if (!entry)
2182
+ return null;
2183
+ return {
2184
+ ref: buildDocRef(entry, results),
2185
+ title: entry.title ?? "",
2186
+ matchCount: entry.matchCount ?? 0,
2187
+ matches: entry.matches?.filter((m) => m?.context).map((m) => {
2188
+ if (!m?.context)
2189
+ return null;
2190
+ return {
2191
+ before: (m.context.before ?? []).filter((l) => l != null),
2192
+ lines: (m.context.matchedLines ?? []).filter((l) => l?.content).map((l) => l?.content ?? ""),
2193
+ after: (m.context.after ?? []).filter((l) => l != null)
2194
+ };
2195
+ }).filter((m) => m != null)
2196
+ };
2197
+ }).filter((e) => e != null);
2198
+ }
2199
+ async function docsSearchAction(queryArg, options, deps) {
2200
+ const { pkgseerService, config } = deps;
2201
+ const searchQuery = queryArg || options.query;
2202
+ const keywords = options.keywords;
2203
+ if (!searchQuery && (!keywords || keywords.length === 0)) {
2204
+ outputError("Search query required. Provide a query as argument or use --query/--keywords", options.json ?? false);
2205
+ return;
2206
+ }
2207
+ const contextBefore = options.context ? Number.parseInt(options.context, 10) : options.before ? Number.parseInt(options.before, 10) : 2;
2208
+ const contextAfter = options.context ? Number.parseInt(options.context, 10) : options.after ? Number.parseInt(options.after, 10) : 2;
2209
+ const maxMatches = options.maxMatches ? Number.parseInt(options.maxMatches, 10) : 5;
2210
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : 25;
2211
+ const matchMode = options.mode?.toUpperCase() || undefined;
2212
+ const useColors = shouldUseColors(options.noColor);
2213
+ const project = options.project ?? config.project;
2214
+ const packageName = options.package;
2215
+ if (packageName) {
2216
+ const registry = toGraphQLRegistry(options.registry ?? "npm");
2217
+ const result = await pkgseerService.cliDocsSearch(registry, packageName, {
2218
+ query: searchQuery,
2219
+ keywords,
2220
+ matchMode,
2221
+ limit,
2222
+ version: options.pkgVersion,
2223
+ contextLinesBefore: contextBefore,
2224
+ contextLinesAfter: contextAfter,
2225
+ maxMatches
2226
+ });
2227
+ handleErrors(result.errors, options.json ?? false);
2228
+ if (!result.data.searchPackageDocs) {
2229
+ outputError(`No documentation found for ${packageName} in ${options.registry ?? "npm"}`, options.json ?? false);
2230
+ return;
2231
+ }
2232
+ outputResults(result.data.searchPackageDocs, options, useColors);
2233
+ return;
2234
+ }
2235
+ if (project) {
2236
+ const result = await pkgseerService.cliProjectDocsSearch(project, {
2237
+ query: searchQuery,
2238
+ keywords,
2239
+ matchMode,
2240
+ limit,
2241
+ contextLinesBefore: contextBefore,
2242
+ contextLinesAfter: contextAfter,
2243
+ maxMatches
2244
+ });
2245
+ handleErrors(result.errors, options.json ?? false);
2246
+ if (!result.data.searchProjectDocs) {
2247
+ outputError(`Project not found: ${project}`, options.json ?? false);
2248
+ return;
2249
+ }
2250
+ outputResults(result.data.searchProjectDocs, options, useColors);
2251
+ return;
2252
+ }
2253
+ outputError(`No project configured. Either:
2254
+ ` + ` - Add project to pkgseer.yml
2255
+ ` + ` - Use --project <name> to specify a project
2256
+ ` + " - Use --package <name> to search a specific package", options.json ?? false);
2257
+ }
2258
+ function outputResults(results, options, useColors) {
2259
+ if (options.json) {
2260
+ output(slimSearchResults(results), true);
2261
+ } else if (options.refsOnly) {
2262
+ console.log(formatRefsOnly(results));
2263
+ } else if (options.count) {
2264
+ console.log(formatCount(results));
2265
+ } else {
2266
+ console.log(formatSearchResults(results, useColors));
2267
+ }
2268
+ }
2269
+ var SEARCH_DESCRIPTION = `Search documentation with grep-like output.
2270
+
2271
+ Searches across package or project documentation with keyword
2272
+ or freeform query support. Shows matching lines with context
2273
+ and highlights matched terms.
2274
+
2275
+ By default, searches project documentation if project is
2276
+ configured in pkgseer.yml. Use --package to search a specific
2277
+ package instead.
2278
+
2279
+ Examples:
2280
+ # Search with context (like grep)
2281
+ pkgseer docs search "error handling" --package express
2282
+ pkgseer docs search log --package express -C 3
2283
+
2284
+ # Multiple keywords (OR by default)
2285
+ pkgseer docs search -k "middleware,routing" --package express
2286
+
2287
+ # Strict matching (AND mode)
2288
+ pkgseer docs search -k "error,middleware" --mode and --package express
2289
+
2290
+ # Output for piping
2291
+ pkgseer docs search log --package express --refs-only | \\
2292
+ xargs -I{} pkgseer docs get express {}
2293
+
2294
+ # Count matches
2295
+ pkgseer docs search error --package express --count`;
2296
+ function addSearchOptions(cmd) {
2297
+ 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");
2298
+ }
2299
+ function registerDocsSearchCommand(program) {
2300
+ const cmd = program.command("search [query]").summary("Search documentation").description(SEARCH_DESCRIPTION);
2301
+ addSearchOptions(cmd).action(async (query, options) => {
2302
+ await withCliErrorHandling(options.json ?? false, async () => {
2303
+ const deps = await createContainer();
2304
+ await docsSearchAction(query, options, deps);
2305
+ });
2306
+ });
2307
+ }
2308
+ // src/commands/login.ts
2309
+ import { hostname } from "node:os";
2310
+ var TIMEOUT_MS = 5 * 60 * 1000;
2311
+ function randomPort() {
2312
+ return Math.floor(Math.random() * 2000) + 8000;
2313
+ }
2314
+ async function loginAction(options, deps) {
2315
+ const { authService, authStorage, browserService, baseUrl } = deps;
2316
+ const existing = await authStorage.load(baseUrl);
2317
+ if (existing && !options.force) {
2318
+ const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
2319
+ if (!isExpired) {
2320
+ console.log(`Already logged in.
2321
+ `);
2322
+ console.log(` Environment: ${baseUrl}`);
2323
+ console.log(` Token: ${existing.tokenName}
2324
+ `);
2325
+ console.log("To switch accounts, run `pkgseer logout` first.");
2326
+ console.log("To re-authenticate with different scopes, use `pkgseer login --force`.");
2327
+ return;
2328
+ }
2329
+ console.log(`Token expired. Starting new login...
2330
+ `);
2331
+ } else if (existing && options.force) {
2332
+ console.log(`Re-authenticating (--force flag)...
2333
+ `);
2334
+ }
2335
+ const { verifier, challenge, state } = authService.generatePkceParams();
2336
+ const port = options.port ?? randomPort();
2337
+ const authUrl = authService.buildAuthUrl({
2338
+ state,
2339
+ port,
2340
+ codeChallenge: challenge,
2341
+ hostname: hostname()
2342
+ });
2343
+ const serverPromise = authService.startCallbackServer(port);
2344
+ if (options.browser === false) {
2345
+ console.log(`Open this URL in your browser:
2346
+ `);
2347
+ console.log(` ${authUrl}
2348
+ `);
2349
+ } else {
2350
+ console.log("Opening browser...");
2351
+ await browserService.open(authUrl);
2352
+ }
2353
+ console.log(`Waiting for authentication...
2354
+ `);
2355
+ let timeoutId;
2356
+ const timeoutPromise = new Promise((_, reject) => {
2357
+ timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
2358
+ });
2359
+ let callback;
2360
+ try {
2361
+ callback = await Promise.race([serverPromise, timeoutPromise]);
2362
+ clearTimeout(timeoutId);
2363
+ } catch (error) {
2364
+ clearTimeout(timeoutId);
2365
+ if (error instanceof Error) {
2366
+ console.log(`${error.message}.
2367
+ `);
2368
+ console.log("Run `pkgseer login` to try again.");
2369
+ }
2370
+ process.exit(1);
2371
+ }
2372
+ if (callback.state !== state) {
2373
+ console.error(`Security error: authentication state mismatch.
2374
+ `);
2375
+ console.log("This could indicate a security issue. Please try again.");
2376
+ process.exit(1);
2377
+ }
2378
+ let tokenResponse;
2379
+ try {
2380
+ tokenResponse = await authService.exchangeCodeForToken({
2381
+ code: callback.code,
2382
+ codeVerifier: verifier,
2383
+ state
2384
+ });
2385
+ } catch (error) {
2386
+ console.error(`Failed to complete authentication: ${error instanceof Error ? error.message : error}
2387
+ `);
2388
+ console.log("Run `pkgseer login` to try again.");
2389
+ process.exit(1);
2390
+ }
2391
+ await authStorage.save(baseUrl, {
2392
+ token: tokenResponse.token,
2393
+ tokenName: tokenResponse.tokenName,
2394
+ scopes: tokenResponse.scopes,
2395
+ createdAt: new Date().toISOString(),
2396
+ expiresAt: tokenResponse.expiresAt,
2397
+ apiKeyId: tokenResponse.apiKeyId
2398
+ });
2399
+ console.log(`✓ Logged in
2400
+ `);
2401
+ console.log(` Environment: ${baseUrl}`);
2402
+ console.log(` Token: ${tokenResponse.tokenName}`);
2403
+ if (tokenResponse.expiresAt) {
2404
+ const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
2405
+ console.log(` Expires: in ${days} days`);
2406
+ }
2407
+ console.log(`
2408
+ You're ready to use pkgseer with your AI assistant.`);
2409
+ }
2410
+ var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
2411
+
2412
+ Opens your browser to complete authentication securely. The CLI receives
2413
+ a token that's stored locally and used for API requests.
2414
+
2415
+ Use --no-browser in environments without a display (CI, SSH sessions)
2416
+ to get a URL you can open on another device.`;
2417
+ function registerLoginCommand(program) {
2418
+ 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) => {
2419
+ const deps = await createContainer();
2420
+ await loginAction(options, deps);
2421
+ });
2422
+ }
2423
+
2424
+ // src/commands/logout.ts
2425
+ async function logoutAction(deps) {
2426
+ const { authService, authStorage, baseUrl } = deps;
2427
+ const auth = await authStorage.load(baseUrl);
2428
+ if (!auth) {
2429
+ console.log(`Not currently logged in.
2430
+ `);
2431
+ console.log(` Environment: ${baseUrl}`);
2432
+ return;
2433
+ }
2434
+ try {
2435
+ await authService.revokeToken(auth.token);
2436
+ } catch {}
2437
+ await authStorage.clear(baseUrl);
2438
+ console.log(`✓ Logged out
2439
+ `);
2440
+ console.log(` Environment: ${baseUrl}`);
2441
+ }
2442
+ var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
2443
+
2444
+ Clears the locally stored authentication token and notifies the server
2445
+ to revoke it. Use this when switching accounts or on shared machines.`;
2446
+ function registerLogoutCommand(program) {
2447
+ program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
2448
+ const deps = await createContainer();
2449
+ await logoutAction(deps);
2450
+ });
2451
+ }
2452
+
2453
+ // src/commands/mcp.ts
2454
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2455
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2456
+
2457
+ // src/tools/compare-packages.ts
2458
+ import { z as z3 } from "zod";
2459
+
2460
+ // src/tools/shared.ts
2461
+ import { z as z2 } from "zod";
2462
+
2463
+ // src/tools/types.ts
2464
+ function textResult(text) {
2465
+ return {
2466
+ content: [{ type: "text", text }]
2467
+ };
2468
+ }
2469
+ function errorResult(message) {
2470
+ return {
2471
+ content: [{ type: "text", text: message }],
2472
+ isError: true
2473
+ };
2474
+ }
2475
+
2476
+ // src/tools/shared.ts
2477
+ function toGraphQLRegistry2(registry) {
2478
+ const map = {
2479
+ npm: "NPM",
2480
+ pypi: "PYPI",
2481
+ hex: "HEX"
2482
+ };
2483
+ return map[registry.toLowerCase()] || "NPM";
2484
+ }
2485
+ var schemas = {
2486
+ registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
2487
+ packageName: z2.string().max(255).describe("Name of the package"),
2488
+ version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
2489
+ };
2490
+ function handleGraphQLErrors(errors) {
2491
+ if (errors && errors.length > 0) {
2492
+ return errorResult(`Error: ${errors.map((e) => e.message).join(", ")}`);
2493
+ }
2494
+ return null;
2495
+ }
2496
+ async function withErrorHandling(operation, fn) {
2497
+ try {
2498
+ return await fn();
2499
+ } catch (error) {
2500
+ const message = error instanceof Error ? error.message : "Unknown error";
2501
+ return errorResult(`Failed to ${operation}: ${message}`);
2502
+ }
2503
+ }
2504
+ function notFoundError(packageName, registry) {
2505
+ return errorResult(`Package not found: ${packageName} in ${registry}`);
2506
+ }
2507
+
2508
+ // src/tools/compare-packages.ts
2509
+ var packageInputSchema = z3.object({
2510
+ registry: z3.enum(["npm", "pypi", "hex"]),
2511
+ name: z3.string().max(255),
2512
+ version: z3.string().max(100).optional()
2513
+ });
2514
+ var argsSchema = {
2515
+ packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
2516
+ };
2517
+ function createComparePackagesTool(pkgseerService) {
2518
+ return {
2519
+ name: "compare_packages",
2520
+ description: "Compares multiple packages across metadata, quality, and security dimensions",
2521
+ schema: argsSchema,
2522
+ handler: async ({ packages }, _extra) => {
2523
+ return withErrorHandling("compare packages", async () => {
2524
+ const input2 = packages.map((pkg) => ({
2525
+ registry: toGraphQLRegistry2(pkg.registry),
2526
+ name: pkg.name,
2527
+ version: pkg.version
2528
+ }));
2529
+ const result = await pkgseerService.comparePackages(input2);
2530
+ const graphqlError = handleGraphQLErrors(result.errors);
2531
+ if (graphqlError)
2532
+ return graphqlError;
2533
+ if (!result.data.comparePackages) {
2534
+ return errorResult("Comparison failed: no results returned");
2535
+ }
2536
+ return textResult(JSON.stringify(result.data.comparePackages, null, 2));
2537
+ });
2538
+ }
2539
+ };
2540
+ }
2541
+ // src/tools/fetch-package-doc.ts
2542
+ import { z as z4 } from "zod";
2543
+ var argsSchema2 = {
2544
+ registry: schemas.registry,
2545
+ package_name: schemas.packageName.describe("Name of the package to fetch documentation for"),
2546
+ page_id: z4.string().max(500).describe("Documentation page identifier (from list_package_docs)"),
2547
+ version: schemas.version
2548
+ };
2549
+ function createFetchPackageDocTool(pkgseerService) {
2550
+ return {
2551
+ name: "fetch_package_doc",
2552
+ 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.",
2553
+ schema: argsSchema2,
2554
+ handler: async ({ registry, package_name, page_id, version: version2 }, _extra) => {
2555
+ return withErrorHandling("fetch documentation page", async () => {
2556
+ const result = await pkgseerService.fetchPackageDoc(toGraphQLRegistry2(registry), package_name, page_id, version2);
2557
+ const graphqlError = handleGraphQLErrors(result.errors);
2558
+ if (graphqlError)
2559
+ return graphqlError;
2560
+ if (!result.data.fetchPackageDoc) {
2561
+ return errorResult(`Documentation page not found: ${page_id} for ${package_name} in ${registry}`);
2562
+ }
2563
+ return textResult(JSON.stringify(result.data.fetchPackageDoc, null, 2));
2564
+ });
2565
+ }
2566
+ };
2567
+ }
2568
+ // src/tools/list-package-docs.ts
2569
+ var argsSchema3 = {
2570
+ registry: schemas.registry,
2571
+ package_name: schemas.packageName.describe("Name of the package to list documentation for"),
2572
+ version: schemas.version
2573
+ };
2574
+ function createListPackageDocsTool(pkgseerService) {
2575
+ return {
2576
+ name: "list_package_docs",
2577
+ description: "Lists documentation pages for a package version. Returns page titles, slugs, and metadata. Use this to discover available documentation before fetching specific pages.",
2578
+ schema: argsSchema3,
2579
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
2580
+ return withErrorHandling("list package documentation", async () => {
2581
+ const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
2582
+ const graphqlError = handleGraphQLErrors(result.errors);
2583
+ if (graphqlError)
2584
+ return graphqlError;
2585
+ if (!result.data.listPackageDocs) {
2586
+ return notFoundError(package_name, registry);
2587
+ }
2588
+ return textResult(JSON.stringify(result.data.listPackageDocs, null, 2));
2589
+ });
2590
+ }
2591
+ };
2592
+ }
2593
+ // src/tools/package-dependencies.ts
2594
+ import { z as z5 } from "zod";
2595
+ var argsSchema4 = {
2596
+ registry: schemas.registry,
2597
+ package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
2598
+ version: schemas.version,
2599
+ include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
2600
+ max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
2601
+ };
2602
+ function createPackageDependenciesTool(pkgseerService) {
2603
+ return {
2604
+ name: "package_dependencies",
2605
+ description: "Retrieves direct and transitive dependencies for a package version",
2606
+ schema: argsSchema4,
2607
+ handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
2608
+ return withErrorHandling("fetch package dependencies", async () => {
2609
+ const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
2610
+ const graphqlError = handleGraphQLErrors(result.errors);
2611
+ if (graphqlError)
2612
+ return graphqlError;
2613
+ if (!result.data.packageDependencies) {
2614
+ return notFoundError(package_name, registry);
2615
+ }
2616
+ return textResult(JSON.stringify(result.data.packageDependencies, null, 2));
2617
+ });
2618
+ }
2619
+ };
2620
+ }
2621
+ // src/tools/package-quality.ts
2622
+ var argsSchema5 = {
2623
+ registry: schemas.registry,
2624
+ package_name: schemas.packageName.describe("Name of the package to analyze"),
2625
+ version: schemas.version
2626
+ };
2627
+ function createPackageQualityTool(pkgseerService) {
2628
+ return {
2629
+ name: "package_quality",
2630
+ description: "Retrieves quality score and rule-level breakdown for a package",
2631
+ schema: argsSchema5,
2632
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
2633
+ return withErrorHandling("fetch package quality", async () => {
2634
+ const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
2635
+ const graphqlError = handleGraphQLErrors(result.errors);
2636
+ if (graphqlError)
2637
+ return graphqlError;
2638
+ if (!result.data.packageQuality) {
2639
+ return notFoundError(package_name, registry);
2640
+ }
2641
+ return textResult(JSON.stringify(result.data.packageQuality, null, 2));
2642
+ });
2643
+ }
2644
+ };
2645
+ }
2646
+ // src/tools/package-summary.ts
2647
+ var argsSchema6 = {
2648
+ registry: schemas.registry,
2649
+ package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
2650
+ };
2651
+ function createPackageSummaryTool(pkgseerService) {
2652
+ return {
2653
+ name: "package_summary",
2654
+ description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
2655
+ schema: argsSchema6,
2656
+ handler: async ({ registry, package_name }, _extra) => {
2657
+ return withErrorHandling("fetch package summary", async () => {
2658
+ const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
2659
+ const graphqlError = handleGraphQLErrors(result.errors);
2660
+ if (graphqlError)
2661
+ return graphqlError;
2662
+ if (!result.data.packageSummary) {
2663
+ return notFoundError(package_name, registry);
2664
+ }
2665
+ return textResult(JSON.stringify(result.data.packageSummary, null, 2));
2666
+ });
2667
+ }
2668
+ };
2669
+ }
2670
+ // src/tools/package-vulnerabilities.ts
2671
+ var argsSchema7 = {
2672
+ registry: schemas.registry,
2673
+ package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
2674
+ version: schemas.version
2675
+ };
2676
+ function createPackageVulnerabilitiesTool(pkgseerService) {
2677
+ return {
2678
+ name: "package_vulnerabilities",
2679
+ description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
2680
+ schema: argsSchema7,
2681
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
2682
+ return withErrorHandling("fetch package vulnerabilities", async () => {
2683
+ const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
2684
+ const graphqlError = handleGraphQLErrors(result.errors);
2685
+ if (graphqlError)
2686
+ return graphqlError;
2687
+ if (!result.data.packageVulnerabilities) {
2688
+ return notFoundError(package_name, registry);
2689
+ }
2690
+ return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
2691
+ });
2692
+ }
2693
+ };
2694
+ }
2695
+ // src/tools/search-package-docs.ts
2696
+ import { z as z6 } from "zod";
2697
+ var argsSchema8 = {
2698
+ registry: schemas.registry,
2699
+ package_name: schemas.packageName.describe("Name of the package to search documentation for"),
2700
+ keywords: z6.array(z6.string()).optional().describe("Keywords to search for; combined into a single query"),
2701
+ query: z6.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
2702
+ include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
2703
+ limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
2704
+ version: schemas.version
2705
+ };
2706
+ function createSearchPackageDocsTool(pkgseerService) {
2707
+ return {
2708
+ name: "search_package_docs",
2709
+ 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.",
2710
+ schema: argsSchema8,
2711
+ handler: async ({
2712
+ registry,
2713
+ package_name,
2714
+ keywords,
2715
+ query,
2716
+ include_snippets,
2717
+ limit,
2718
+ version: version2
2719
+ }, _extra) => {
2720
+ return withErrorHandling("search package documentation", async () => {
2721
+ if (!keywords?.length && !query) {
2722
+ return errorResult("Either keywords or query must be provided for search");
2723
+ }
2724
+ const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
2725
+ keywords,
2726
+ query,
2727
+ includeSnippets: include_snippets,
2728
+ limit,
2729
+ version: version2
2730
+ });
2731
+ const graphqlError = handleGraphQLErrors(result.errors);
2732
+ if (graphqlError)
2733
+ return graphqlError;
2734
+ if (!result.data.searchPackageDocs) {
2735
+ return errorResult(`No documentation found for ${package_name} in ${registry}`);
2736
+ }
2737
+ return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
2738
+ });
2739
+ }
2740
+ };
2741
+ }
2742
+ // src/tools/search-project-docs.ts
2743
+ import { z as z7 } from "zod";
2744
+ var argsSchema9 = {
2745
+ project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
2746
+ keywords: z7.array(z7.string()).optional().describe("Keywords to search for; combined into a single query"),
2747
+ query: z7.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
2748
+ include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
2749
+ limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
2750
+ };
2751
+ function createSearchProjectDocsTool(deps) {
2752
+ const { pkgseerService, config } = deps;
2753
+ return {
2754
+ name: "search_project_docs",
2755
+ description: "Searches documentation across all dependencies in a PkgSeer project. Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
2756
+ schema: argsSchema9,
2757
+ handler: async ({ project, keywords, query, include_snippets, limit }, _extra) => {
2758
+ return withErrorHandling("search project documentation", async () => {
2759
+ const resolvedProject = project ?? config.project;
2760
+ if (!resolvedProject) {
2761
+ return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
2762
+ }
2763
+ if (!keywords?.length && !query) {
2764
+ return errorResult("Either keywords or query must be provided for search");
2765
+ }
2766
+ const result = await pkgseerService.searchProjectDocs(resolvedProject, {
2767
+ keywords,
2768
+ query,
2769
+ includeSnippets: include_snippets,
2770
+ limit
2771
+ });
2772
+ const graphqlError = handleGraphQLErrors(result.errors);
2773
+ if (graphqlError)
2774
+ return graphqlError;
2775
+ if (!result.data.searchProjectDocs) {
2776
+ return errorResult(`Project not found: ${resolvedProject}`);
2777
+ }
2778
+ return textResult(JSON.stringify(result.data.searchProjectDocs, null, 2));
2779
+ });
2780
+ }
2781
+ };
2782
+ }
2783
+ // src/commands/mcp.ts
2784
+ var TOOL_FACTORIES = {
2785
+ package_summary: ({ pkgseerService }) => createPackageSummaryTool(pkgseerService),
2786
+ package_vulnerabilities: ({ pkgseerService }) => createPackageVulnerabilitiesTool(pkgseerService),
2787
+ package_dependencies: ({ pkgseerService }) => createPackageDependenciesTool(pkgseerService),
2788
+ package_quality: ({ pkgseerService }) => createPackageQualityTool(pkgseerService),
2789
+ compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
2790
+ list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
2791
+ fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
2792
+ search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
2793
+ search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
2794
+ };
2795
+ var PUBLIC_READ_TOOLS = [
2796
+ "package_summary",
2797
+ "package_vulnerabilities",
2798
+ "package_dependencies",
2799
+ "package_quality",
2800
+ "compare_packages",
2801
+ "list_package_docs",
2802
+ "fetch_package_doc",
2803
+ "search_package_docs"
2804
+ ];
2805
+ var PROJECT_READ_TOOLS = ["search_project_docs"];
2806
+ var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
2807
+ function createMcpServer(deps) {
2808
+ const { pkgseerService, config } = deps;
2809
+ const server = new McpServer({
2810
+ name: "pkgseer",
2811
+ version: "0.1.0"
2812
+ });
2813
+ const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
2814
+ const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
2815
+ for (const toolName of toolsToRegister) {
2816
+ const factory = TOOL_FACTORIES[toolName];
2817
+ const tool = factory({ pkgseerService, config });
2818
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
2819
+ }
2820
+ return server;
2821
+ }
2822
+ function requireAuth(deps) {
2823
+ if (deps.hasValidToken) {
2824
+ return;
2825
+ }
2826
+ console.log(`Authentication required to start MCP server.
2827
+ `);
2828
+ if (deps.baseUrl !== "https://pkgseer.dev") {
2829
+ console.log(` Environment: ${deps.baseUrl}`);
2830
+ console.log(` You're using a custom environment.
2831
+ `);
2832
+ }
2833
+ console.log("To authenticate:");
2834
+ console.log(` pkgseer login
2835
+ `);
2836
+ console.log("Or set PKGSEER_API_TOKEN environment variable.");
2837
+ process.exit(1);
2838
+ }
2839
+ async function startMcpServer(deps) {
2840
+ requireAuth(deps);
2841
+ const server = createMcpServer(deps);
2842
+ const transport = new StdioServerTransport;
2843
+ await server.connect(transport);
2844
+ }
2845
+ function registerMcpCommand(program) {
2846
+ program.command("mcp").summary("Start MCP server for AI assistants").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
2847
+
2848
+ This allows AI assistants like Claude, Cursor, and others to query package
2849
+ information directly. Add this to your assistant's MCP configuration:
2850
+
2851
+ "pkgseer": {
2852
+ "command": "pkgseer",
2853
+ "args": ["mcp"]
2854
+ }
2855
+
2856
+ Available tools: package_summary, package_vulnerabilities,
2857
+ package_dependencies, package_quality, compare_packages,
2858
+ list_package_docs, fetch_package_doc, search_package_docs,
2859
+ search_project_docs`).action(async () => {
802
2860
  const deps = await createContainer();
803
- await logoutAction(deps);
2861
+ await startMcpServer(deps);
804
2862
  });
805
2863
  }
806
2864
 
807
- // src/commands/mcp.ts
808
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
809
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2865
+ // src/commands/pkg/compare.ts
2866
+ function parsePackageSpec(spec) {
2867
+ let registry = "npm";
2868
+ let rest = spec;
2869
+ if (spec.includes(":")) {
2870
+ const colonIndex = spec.indexOf(":");
2871
+ const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
2872
+ if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
2873
+ registry = potentialRegistry;
2874
+ rest = spec.slice(colonIndex + 1);
2875
+ }
2876
+ }
2877
+ const atIndex = rest.lastIndexOf("@");
2878
+ if (atIndex > 0) {
2879
+ return {
2880
+ registry,
2881
+ name: rest.slice(0, atIndex),
2882
+ version: rest.slice(atIndex + 1)
2883
+ };
2884
+ }
2885
+ return { registry, name: rest };
2886
+ }
2887
+ function formatPackageComparison(comparison) {
2888
+ const lines = [];
2889
+ lines.push("\uD83D\uDCCA Package Comparison");
2890
+ lines.push("");
2891
+ if (!comparison.packages || comparison.packages.length === 0) {
2892
+ lines.push("No packages to compare.");
2893
+ return lines.join(`
2894
+ `);
2895
+ }
2896
+ const packages = comparison.packages.filter((p) => p != null);
2897
+ const maxNameLen = Math.max(...packages.map((p) => `${p.packageName}@${p.version}`.length));
2898
+ const header = "Package".padEnd(maxNameLen + 2);
2899
+ lines.push(` ${header} Quality Downloads/mo Vulns`);
2900
+ lines.push(` ${"─".repeat(maxNameLen + 2)} ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(5)}`);
2901
+ for (const pkg of packages) {
2902
+ const name = `${pkg.packageName}@${pkg.version}`.padEnd(maxNameLen + 2);
2903
+ const scoreValue = pkg.quality?.score;
2904
+ const quality = scoreValue != null ? `${Math.round(scoreValue > 1 ? scoreValue : scoreValue * 100)}%`.padEnd(10) : "N/A".padEnd(10);
2905
+ const downloads = pkg.downloadsLastMonth ? formatNumber(pkg.downloadsLastMonth).padEnd(12) : "N/A".padEnd(12);
2906
+ const vulns = pkg.vulnerabilityCount != null ? pkg.vulnerabilityCount === 0 ? "✅ 0" : `⚠️ ${pkg.vulnerabilityCount}` : "N/A";
2907
+ lines.push(` ${name} ${quality} ${downloads} ${vulns}`);
2908
+ }
2909
+ return lines.join(`
2910
+ `);
2911
+ }
2912
+ async function pkgCompareAction(packages, options, deps) {
2913
+ const { pkgseerService } = deps;
2914
+ if (packages.length < 2) {
2915
+ outputError("At least 2 packages required for comparison", options.json ?? false);
2916
+ return;
2917
+ }
2918
+ if (packages.length > 10) {
2919
+ outputError("Maximum 10 packages can be compared at once", options.json ?? false);
2920
+ return;
2921
+ }
2922
+ const input2 = packages.map((spec) => {
2923
+ const parsed = parsePackageSpec(spec);
2924
+ return {
2925
+ registry: toGraphQLRegistry(parsed.registry),
2926
+ name: parsed.name,
2927
+ version: parsed.version
2928
+ };
2929
+ });
2930
+ const result = await pkgseerService.cliComparePackages(input2);
2931
+ handleErrors(result.errors, options.json ?? false);
2932
+ if (!result.data.comparePackages) {
2933
+ outputError("Comparison failed", options.json ?? false);
2934
+ return;
2935
+ }
2936
+ if (options.json) {
2937
+ const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
2938
+ const slim = pkgs.map((p) => ({
2939
+ package: `${p.packageName}@${p.version}`,
2940
+ quality: p.quality?.score,
2941
+ downloads: p.downloadsLastMonth,
2942
+ vulnerabilities: p.vulnerabilityCount
2943
+ }));
2944
+ output(slim, true);
2945
+ } else {
2946
+ console.log(formatPackageComparison(result.data.comparePackages));
2947
+ }
2948
+ }
2949
+ var COMPARE_DESCRIPTION = `Compare multiple packages.
810
2950
 
811
- // src/tools/types.ts
812
- function textResult(text) {
813
- return {
814
- content: [{ type: "text", text }]
815
- };
2951
+ Compares packages across quality, security, and popularity metrics.
2952
+ Supports cross-registry comparison.
2953
+
2954
+ Package format: [registry:]name[@version]
2955
+ - lodash (npm, latest)
2956
+ - pypi:requests (pypi, latest)
2957
+ - npm:express@4.18.0 (npm, specific version)
2958
+
2959
+ Examples:
2960
+ pkgseer pkg compare lodash underscore ramda
2961
+ pkgseer pkg compare npm:axios pypi:requests
2962
+ pkgseer pkg compare express@4.18.0 express@5.0.0 --json`;
2963
+ function registerPkgCompareCommand(program) {
2964
+ program.command("compare <packages...>").summary("Compare multiple packages").description(COMPARE_DESCRIPTION).option("--json", "Output as JSON").action(async (packages, options) => {
2965
+ await withCliErrorHandling(options.json ?? false, async () => {
2966
+ const deps = await createContainer();
2967
+ await pkgCompareAction(packages, options, deps);
2968
+ });
2969
+ });
816
2970
  }
817
- function errorResult(message) {
818
- return {
819
- content: [{ type: "text", text: message }],
820
- isError: true
821
- };
2971
+ // src/commands/pkg/deps.ts
2972
+ function formatPackageDependencies(data) {
2973
+ const lines = [];
2974
+ const pkg = data.package;
2975
+ const deps = data.dependencies;
2976
+ if (!pkg) {
2977
+ return "No package data available.";
2978
+ }
2979
+ lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.version}`);
2980
+ lines.push("");
2981
+ if (deps?.summary) {
2982
+ lines.push("Summary:");
2983
+ lines.push(` Direct dependencies: ${deps.summary.directCount ?? 0}`);
2984
+ if (deps.summary.uniquePackagesCount) {
2985
+ lines.push(` Unique packages: ${deps.summary.uniquePackagesCount}`);
2986
+ }
2987
+ lines.push("");
2988
+ }
2989
+ if (deps?.direct && deps.direct.length > 0) {
2990
+ lines.push(`Dependencies (${deps.direct.length}):`);
2991
+ for (const dep of deps.direct) {
2992
+ if (dep) {
2993
+ const type = dep.type !== "RUNTIME" ? ` [${dep.type?.toLowerCase()}]` : "";
2994
+ lines.push(` ${dep.name} ${dep.versionConstraint}${type}`);
2995
+ }
2996
+ }
2997
+ } else {
2998
+ lines.push("No direct dependencies.");
2999
+ }
3000
+ return lines.join(`
3001
+ `);
822
3002
  }
823
- // src/tools/shared.ts
824
- import { z } from "zod";
825
- function toGraphQLRegistry(registry) {
826
- const map = {
827
- npm: "NPM",
828
- pypi: "PYPI",
829
- hex: "HEX"
3003
+ async function pkgDepsAction(packageName, options, deps) {
3004
+ const { pkgseerService } = deps;
3005
+ const registry = toGraphQLRegistry(options.registry);
3006
+ const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive);
3007
+ handleErrors(result.errors, options.json ?? false);
3008
+ if (!result.data.packageDependencies) {
3009
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3010
+ return;
3011
+ }
3012
+ if (options.json) {
3013
+ const data = result.data.packageDependencies;
3014
+ const slim = {
3015
+ package: `${data.package?.name}@${data.package?.version}`,
3016
+ directCount: data.dependencies?.summary?.directCount ?? 0,
3017
+ dependencies: data.dependencies?.direct?.filter((d) => d).map((d) => ({
3018
+ name: d.name,
3019
+ version: d.versionConstraint,
3020
+ type: d.type
3021
+ }))
3022
+ };
3023
+ output(slim, true);
3024
+ } else {
3025
+ console.log(formatPackageDependencies(result.data.packageDependencies));
3026
+ }
3027
+ }
3028
+ var DEPS_DESCRIPTION = `Get package dependencies.
3029
+
3030
+ Lists direct dependencies and shows version constraints and
3031
+ dependency types (runtime, dev, optional).
3032
+
3033
+ Examples:
3034
+ pkgseer pkg deps express
3035
+ pkgseer pkg deps lodash --transitive
3036
+ pkgseer pkg deps requests --registry pypi --json`;
3037
+ function registerPkgDepsCommand(program) {
3038
+ 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) => {
3039
+ await withCliErrorHandling(options.json ?? false, async () => {
3040
+ const deps = await createContainer();
3041
+ await pkgDepsAction(packageName, options, deps);
3042
+ });
3043
+ });
3044
+ }
3045
+ // src/commands/pkg/info.ts
3046
+ function formatPackageSummary(data) {
3047
+ const lines = [];
3048
+ const pkg = data.package;
3049
+ if (!pkg) {
3050
+ return "No package data available.";
3051
+ }
3052
+ lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.latestVersion}`);
3053
+ lines.push("");
3054
+ if (pkg.description) {
3055
+ lines.push(pkg.description);
3056
+ lines.push("");
3057
+ }
3058
+ lines.push("Package Info:");
3059
+ lines.push(keyValueTable([
3060
+ ["Registry", pkg.registry],
3061
+ ["Version", pkg.latestVersion],
3062
+ ["License", pkg.license]
3063
+ ]));
3064
+ lines.push("");
3065
+ if (pkg.homepage || pkg.repositoryUrl) {
3066
+ lines.push("Links:");
3067
+ const links = [];
3068
+ if (pkg.homepage)
3069
+ links.push(["Homepage", pkg.homepage]);
3070
+ if (pkg.repositoryUrl)
3071
+ links.push(["Repository", pkg.repositoryUrl]);
3072
+ lines.push(keyValueTable(links));
3073
+ lines.push("");
3074
+ }
3075
+ const vulnCount = data.security?.vulnerabilityCount ?? 0;
3076
+ if (vulnCount > 0) {
3077
+ lines.push(`⚠️ Security: ${vulnCount} vulnerabilities`);
3078
+ lines.push("");
3079
+ }
3080
+ if (data.quickstart?.installCommand) {
3081
+ lines.push("Quick Start:");
3082
+ lines.push(` ${data.quickstart.installCommand}`);
3083
+ }
3084
+ return lines.join(`
3085
+ `);
3086
+ }
3087
+ async function pkgInfoAction(packageName, options, deps) {
3088
+ const { pkgseerService } = deps;
3089
+ const registry = toGraphQLRegistry(options.registry);
3090
+ const result = await pkgseerService.cliPackageInfo(registry, packageName);
3091
+ handleErrors(result.errors, options.json ?? false);
3092
+ if (!result.data.packageSummary) {
3093
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3094
+ return;
3095
+ }
3096
+ if (options.json) {
3097
+ const data = result.data.packageSummary;
3098
+ const pkg = data.package;
3099
+ const slim = {
3100
+ name: pkg?.name,
3101
+ version: pkg?.latestVersion,
3102
+ description: pkg?.description,
3103
+ license: pkg?.license,
3104
+ install: data.quickstart?.installCommand,
3105
+ vulnerabilities: data.security?.vulnerabilityCount ?? 0
3106
+ };
3107
+ output(slim, true);
3108
+ } else {
3109
+ console.log(formatPackageSummary(result.data.packageSummary));
3110
+ }
3111
+ }
3112
+ var INFO_DESCRIPTION = `Get package summary and metadata.
3113
+
3114
+ Displays comprehensive information about a package including:
3115
+ - Basic metadata (version, license, description)
3116
+ - Download statistics
3117
+ - Security advisories
3118
+ - Quick start instructions
3119
+
3120
+ Examples:
3121
+ pkgseer pkg info lodash
3122
+ pkgseer pkg info requests --registry pypi
3123
+ pkgseer pkg info phoenix --registry hex --json`;
3124
+ function registerPkgInfoCommand(program) {
3125
+ 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) => {
3126
+ await withCliErrorHandling(options.json ?? false, async () => {
3127
+ const deps = await createContainer();
3128
+ await pkgInfoAction(packageName, options, deps);
3129
+ });
3130
+ });
3131
+ }
3132
+ // src/commands/pkg/quality.ts
3133
+ function formatPackageQuality(data) {
3134
+ const lines = [];
3135
+ const quality = data.quality;
3136
+ if (!quality) {
3137
+ return "No quality data available.";
3138
+ }
3139
+ lines.push(`\uD83D\uDCCA Quality Score: ${formatScore(quality.overallScore)} (Grade: ${quality.grade})`);
3140
+ lines.push("");
3141
+ if (quality.categories && quality.categories.length > 0) {
3142
+ lines.push("Category Breakdown:");
3143
+ for (const category of quality.categories) {
3144
+ if (category) {
3145
+ const name = (category.category || "Unknown").padEnd(20);
3146
+ lines.push(` ${name} ${formatScore(category.score)}`);
3147
+ }
3148
+ }
3149
+ lines.push("");
3150
+ }
3151
+ return lines.join(`
3152
+ `);
3153
+ }
3154
+ async function pkgQualityAction(packageName, options, deps) {
3155
+ const { pkgseerService } = deps;
3156
+ const registry = toGraphQLRegistry(options.registry);
3157
+ const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
3158
+ handleErrors(result.errors, options.json ?? false);
3159
+ if (!result.data.packageQuality) {
3160
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3161
+ return;
3162
+ }
3163
+ if (options.json) {
3164
+ const quality = result.data.packageQuality.quality;
3165
+ const slim = {
3166
+ package: `${result.data.packageQuality.package?.name}@${result.data.packageQuality.package?.version}`,
3167
+ score: quality?.overallScore,
3168
+ grade: quality?.grade,
3169
+ categories: quality?.categories?.filter((c) => c).map((c) => ({
3170
+ name: c.category,
3171
+ score: c.score
3172
+ }))
3173
+ };
3174
+ output(slim, true);
3175
+ } else {
3176
+ console.log(formatPackageQuality(result.data.packageQuality));
3177
+ }
3178
+ }
3179
+ var QUALITY_DESCRIPTION = `Get package quality score and breakdown.
3180
+
3181
+ Analyzes package quality across multiple dimensions:
3182
+ - Maintenance and activity
3183
+ - Documentation coverage
3184
+ - Security practices
3185
+ - Community engagement
3186
+
3187
+ Examples:
3188
+ pkgseer pkg quality lodash
3189
+ pkgseer pkg quality express -v 4.18.0
3190
+ pkgseer pkg quality requests --registry pypi --json`;
3191
+ function registerPkgQualityCommand(program) {
3192
+ 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) => {
3193
+ await withCliErrorHandling(options.json ?? false, async () => {
3194
+ const deps = await createContainer();
3195
+ await pkgQualityAction(packageName, options, deps);
3196
+ });
3197
+ });
3198
+ }
3199
+ // src/commands/pkg/vulns.ts
3200
+ function getSeverityLabel(score) {
3201
+ if (score == null)
3202
+ return "UNKNOWN";
3203
+ if (score >= 9)
3204
+ return "CRITICAL";
3205
+ if (score >= 7)
3206
+ return "HIGH";
3207
+ if (score >= 4)
3208
+ return "MEDIUM";
3209
+ return "LOW";
3210
+ }
3211
+ function formatPackageVulnerabilities(data) {
3212
+ const lines = [];
3213
+ const pkg = data.package;
3214
+ const security = data.security;
3215
+ if (!pkg) {
3216
+ return "No package data available.";
3217
+ }
3218
+ lines.push(`\uD83D\uDD12 Security Report: ${pkg.name}@${pkg.version}`);
3219
+ lines.push("");
3220
+ if (!security?.vulnerabilities || security.vulnerabilities.length === 0) {
3221
+ lines.push("✅ No known vulnerabilities!");
3222
+ return lines.join(`
3223
+ `);
3224
+ }
3225
+ const bySeverity = security.vulnerabilities.reduce((acc, v) => {
3226
+ if (v) {
3227
+ const label = getSeverityLabel(v.severityScore);
3228
+ acc[label] = (acc[label] || 0) + 1;
3229
+ }
3230
+ return acc;
3231
+ }, {});
3232
+ lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
3233
+ for (const [severity, count] of Object.entries(bySeverity)) {
3234
+ lines.push(` ${formatSeverity(severity)}: ${count}`);
3235
+ }
3236
+ lines.push("");
3237
+ lines.push("Details:");
3238
+ for (const vuln of security.vulnerabilities) {
3239
+ if (!vuln)
3240
+ continue;
3241
+ lines.push("");
3242
+ lines.push(` ${formatSeverity(getSeverityLabel(vuln.severityScore))}`);
3243
+ lines.push(` ${vuln.summary || vuln.osvId}`);
3244
+ lines.push(` ID: ${vuln.osvId}`);
3245
+ if (vuln.fixedInVersions && vuln.fixedInVersions.length > 0) {
3246
+ lines.push(` Fixed in: ${vuln.fixedInVersions.join(", ")}`);
3247
+ }
3248
+ }
3249
+ return lines.join(`
3250
+ `);
3251
+ }
3252
+ async function pkgVulnsAction(packageName, options, deps) {
3253
+ const { pkgseerService } = deps;
3254
+ const registry = toGraphQLRegistry(options.registry);
3255
+ const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
3256
+ handleErrors(result.errors, options.json ?? false);
3257
+ if (!result.data.packageVulnerabilities) {
3258
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3259
+ return;
3260
+ }
3261
+ if (options.json) {
3262
+ const data = result.data.packageVulnerabilities;
3263
+ const vulns = data.security?.vulnerabilities ?? [];
3264
+ const slim = {
3265
+ package: `${data.package?.name}@${data.package?.version}`,
3266
+ count: data.security?.vulnerabilityCount ?? 0,
3267
+ vulnerabilities: vulns.filter((v) => v).map((v) => ({
3268
+ id: v.osvId,
3269
+ severity: v.severityScore,
3270
+ summary: v.summary,
3271
+ fixed: v.fixedInVersions
3272
+ }))
3273
+ };
3274
+ output(slim, true);
3275
+ } else {
3276
+ console.log(formatPackageVulnerabilities(result.data.packageVulnerabilities));
3277
+ }
3278
+ }
3279
+ var VULNS_DESCRIPTION = `Check package for security vulnerabilities.
3280
+
3281
+ Scans for known security vulnerabilities and provides:
3282
+ - Severity ratings (critical, high, medium, low)
3283
+ - CVE identifiers
3284
+ - Affected version ranges
3285
+ - Upgrade recommendations
3286
+
3287
+ Examples:
3288
+ pkgseer pkg vulns lodash
3289
+ pkgseer pkg vulns express -v 4.17.0
3290
+ pkgseer pkg vulns requests --registry pypi --json`;
3291
+ function registerPkgVulnsCommand(program) {
3292
+ 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) => {
3293
+ await withCliErrorHandling(options.json ?? false, async () => {
3294
+ const deps = await createContainer();
3295
+ await pkgVulnsAction(packageName, options, deps);
3296
+ });
3297
+ });
3298
+ }
3299
+ // src/services/gitignore-parser.ts
3300
+ function parseGitIgnoreLine(line) {
3301
+ const trimmed = line.trimEnd();
3302
+ if (!trimmed || trimmed.startsWith("#")) {
3303
+ return null;
3304
+ }
3305
+ const isNegation = trimmed.startsWith("!");
3306
+ const pattern = isNegation ? trimmed.slice(1) : trimmed;
3307
+ const isRootOnly = pattern.startsWith("/");
3308
+ const patternWithoutRoot = isRootOnly ? pattern.slice(1) : pattern;
3309
+ const isDirectory = patternWithoutRoot.endsWith("/");
3310
+ const cleanPattern = isDirectory ? patternWithoutRoot.slice(0, -1) : patternWithoutRoot;
3311
+ return {
3312
+ pattern: cleanPattern,
3313
+ isNegation,
3314
+ isDirectory,
3315
+ isRootOnly
830
3316
  };
831
- return map[registry.toLowerCase()] || "NPM";
832
3317
  }
833
- var schemas = {
834
- registry: z.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
835
- packageName: z.string().max(255).describe("Name of the package"),
836
- version: z.string().max(100).optional().describe("Specific version (defaults to latest)")
837
- };
838
- function handleGraphQLErrors(errors) {
839
- if (errors && errors.length > 0) {
840
- return errorResult(`Error: ${errors.map((e) => e.message).join(", ")}`);
3318
+ function matchesPattern(path, pattern) {
3319
+ const { pattern: p, isDirectory, isRootOnly } = pattern;
3320
+ let regexStr = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/___DOUBLE_STAR___/g, ".*");
3321
+ if (isRootOnly) {
3322
+ if (isDirectory) {
3323
+ regexStr = `^${regexStr}(/|$)`;
3324
+ } else {
3325
+ regexStr = `^${regexStr}(/|$)`;
3326
+ }
3327
+ } else {
3328
+ if (isDirectory) {
3329
+ regexStr = `(^|/)${regexStr}(/|$)`;
3330
+ } else {
3331
+ regexStr = `(^|/)${regexStr}(/|$)`;
3332
+ }
841
3333
  }
842
- return null;
3334
+ const regex = new RegExp(regexStr);
3335
+ return regex.test(path);
843
3336
  }
844
- async function withErrorHandling(operation, fn) {
3337
+ async function parseGitIgnore(gitignorePath, fs) {
845
3338
  try {
846
- return await fn();
847
- } catch (error) {
848
- const message = error instanceof Error ? error.message : "Unknown error";
849
- return errorResult(`Failed to ${operation}: ${message}`);
3339
+ const content = await fs.readFile(gitignorePath);
3340
+ const lines = content.split(`
3341
+ `);
3342
+ const patterns = [];
3343
+ for (const line of lines) {
3344
+ const pattern = parseGitIgnoreLine(line);
3345
+ if (pattern) {
3346
+ patterns.push(pattern);
3347
+ }
3348
+ }
3349
+ return patterns.length > 0 ? patterns : null;
3350
+ } catch {
3351
+ return null;
850
3352
  }
851
3353
  }
852
- function notFoundError(packageName, registry) {
853
- return errorResult(`Package not found: ${packageName} in ${registry}`);
3354
+ function shouldIgnorePath(relativePath, patterns) {
3355
+ const normalized = relativePath.replace(/\\/g, "/");
3356
+ let ignored = false;
3357
+ for (const pattern of patterns) {
3358
+ if (matchesPattern(normalized, pattern)) {
3359
+ if (pattern.isNegation) {
3360
+ ignored = false;
3361
+ } else {
3362
+ ignored = true;
3363
+ }
3364
+ }
3365
+ }
3366
+ return ignored;
854
3367
  }
855
- // src/tools/package-summary.ts
856
- var argsSchema = {
857
- registry: schemas.registry,
858
- package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
859
- };
860
- function createPackageSummaryTool(pkgseerService) {
861
- return {
862
- name: "package_summary",
863
- description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
864
- schema: argsSchema,
865
- handler: async ({ registry, package_name }, _extra) => {
866
- return withErrorHandling("fetch package summary", async () => {
867
- const result = await pkgseerService.getPackageSummary(toGraphQLRegistry(registry), package_name);
868
- const graphqlError = handleGraphQLErrors(result.errors);
869
- if (graphqlError)
870
- return graphqlError;
871
- if (!result.data.packageSummary) {
872
- return notFoundError(package_name, registry);
3368
+ function shouldIgnoreDirectory(relativeDirPath, patterns) {
3369
+ const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
3370
+ return shouldIgnorePath(normalized, patterns);
3371
+ }
3372
+
3373
+ // src/services/manifest-detector.ts
3374
+ var DEFAULT_EXCLUDED_DIRS = [
3375
+ "node_modules",
3376
+ "vendor",
3377
+ ".git",
3378
+ "dist",
3379
+ "build",
3380
+ "__pycache__",
3381
+ ".venv",
3382
+ "venv",
3383
+ ".next"
3384
+ ];
3385
+ var MANIFEST_TYPES = [
3386
+ {
3387
+ type: "npm",
3388
+ filenames: [
3389
+ "package.json",
3390
+ "package-lock.json",
3391
+ "yarn.lock",
3392
+ "pnpm-lock.yaml"
3393
+ ]
3394
+ },
3395
+ {
3396
+ type: "pypi",
3397
+ filenames: ["requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock"]
3398
+ },
3399
+ {
3400
+ type: "hex",
3401
+ filenames: ["mix.exs", "mix.lock"]
3402
+ }
3403
+ ];
3404
+ function suggestLabel(relativePath) {
3405
+ const normalized = relativePath.replace(/\\/g, "/");
3406
+ const parts = normalized.split("/").filter((p) => p.length > 0);
3407
+ if (parts.length === 1) {
3408
+ return "root";
3409
+ }
3410
+ return parts[parts.length - 2];
3411
+ }
3412
+ async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
3413
+ const detected = [];
3414
+ if (currentDepth > options.maxDepth) {
3415
+ return detected;
3416
+ }
3417
+ const relativeDirPath = directory === rootDir ? "." : directory.replace(rootDir, "").replace(/^[/\\]/, "");
3418
+ const baseName = directory.split(/[/\\]/).pop() ?? "";
3419
+ if (options.excludedDirs.includes(baseName)) {
3420
+ return detected;
3421
+ }
3422
+ if (gitignorePatterns && shouldIgnoreDirectory(relativeDirPath, gitignorePatterns)) {
3423
+ return detected;
3424
+ }
3425
+ for (const manifestType of MANIFEST_TYPES) {
3426
+ for (const filename of manifestType.filenames) {
3427
+ const filePath = fs.joinPath(directory, filename);
3428
+ const exists = await fs.exists(filePath);
3429
+ if (exists) {
3430
+ const relativePath = directory === rootDir ? filename : fs.joinPath(directory.replace(rootDir, "").replace(/^[/\\]/, ""), filename);
3431
+ if (gitignorePatterns && shouldIgnorePath(relativePath, gitignorePatterns)) {
3432
+ continue;
873
3433
  }
874
- return textResult(JSON.stringify(result.data.packageSummary, null, 2));
875
- });
3434
+ detected.push({
3435
+ filename,
3436
+ relativePath,
3437
+ absolutePath: filePath,
3438
+ type: manifestType.type,
3439
+ suggestedLabel: suggestLabel(relativePath)
3440
+ });
3441
+ }
3442
+ }
3443
+ }
3444
+ try {
3445
+ const entries = await fs.readdir(directory);
3446
+ for (const entry of entries) {
3447
+ const entryPath = fs.joinPath(directory, entry);
3448
+ const isDir = await fs.isDirectory(entryPath);
3449
+ if (isDir && !options.excludedDirs.includes(entry)) {
3450
+ const subRelativePath = fs.joinPath(relativeDirPath, entry);
3451
+ if (!gitignorePatterns || !shouldIgnoreDirectory(subRelativePath, gitignorePatterns)) {
3452
+ const subDetected = await scanDirectoryRecursive(entryPath, fs, rootDir, options, currentDepth + 1, gitignorePatterns);
3453
+ detected.push(...subDetected);
3454
+ }
3455
+ }
876
3456
  }
3457
+ } catch {}
3458
+ return detected;
3459
+ }
3460
+ async function scanForManifests(directory, fs, options) {
3461
+ const opts = {
3462
+ maxDepth: options?.maxDepth ?? 3,
3463
+ excludedDirs: options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS
877
3464
  };
3465
+ const gitignorePath = fs.joinPath(directory, ".gitignore");
3466
+ const gitignorePatterns = await parseGitIgnore(gitignorePath, fs);
3467
+ return scanDirectoryRecursive(directory, fs, directory, opts, 0, gitignorePatterns);
878
3468
  }
879
- // src/tools/package-vulnerabilities.ts
880
- var argsSchema2 = {
881
- registry: schemas.registry,
882
- package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
883
- version: schemas.version
884
- };
885
- function createPackageVulnerabilitiesTool(pkgseerService) {
886
- return {
887
- name: "package_vulnerabilities",
888
- description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
889
- schema: argsSchema2,
890
- handler: async ({ registry, package_name, version: version2 }, _extra) => {
891
- return withErrorHandling("fetch package vulnerabilities", async () => {
892
- const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry(registry), package_name, version2);
893
- const graphqlError = handleGraphQLErrors(result.errors);
894
- if (graphqlError)
895
- return graphqlError;
896
- if (!result.data.packageVulnerabilities) {
897
- return notFoundError(package_name, registry);
898
- }
899
- return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
3469
+ function filterRedundantPackageJson(manifests) {
3470
+ const dirToManifests = new Map;
3471
+ for (const manifest of manifests) {
3472
+ const dir = manifest.relativePath.split(/[/\\]/).slice(0, -1).join("/") || ".";
3473
+ if (!dirToManifests.has(dir)) {
3474
+ dirToManifests.set(dir, []);
3475
+ }
3476
+ dirToManifests.get(dir).push(manifest);
3477
+ }
3478
+ const filtered = [];
3479
+ for (const [dir, dirManifests] of dirToManifests.entries()) {
3480
+ const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
3481
+ const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3482
+ for (const manifest of dirManifests) {
3483
+ if (manifest.filename === "package.json" && hasLockFile) {
3484
+ continue;
3485
+ }
3486
+ filtered.push(manifest);
3487
+ }
3488
+ }
3489
+ return filtered;
3490
+ }
3491
+ async function detectAndGroupManifests(directory, fs, options) {
3492
+ const manifests = await scanForManifests(directory, fs, options);
3493
+ const filteredManifests = filterRedundantPackageJson(manifests);
3494
+ const groupsMap = new Map;
3495
+ for (const manifest of filteredManifests) {
3496
+ const existing = groupsMap.get(manifest.suggestedLabel) ?? [];
3497
+ existing.push(manifest);
3498
+ groupsMap.set(manifest.suggestedLabel, existing);
3499
+ }
3500
+ const groups = [];
3501
+ for (const [label, manifests2] of groupsMap.entries()) {
3502
+ const ecosystems = new Set(manifests2.map((m) => m.type));
3503
+ if (ecosystems.size > 1) {
3504
+ const ecosystemGroups = new Map;
3505
+ for (const manifest of manifests2) {
3506
+ const ecosystemLabel = `${label}-${manifest.type}`;
3507
+ const existing = ecosystemGroups.get(ecosystemLabel) ?? [];
3508
+ existing.push(manifest);
3509
+ ecosystemGroups.set(ecosystemLabel, existing);
3510
+ }
3511
+ for (const [
3512
+ ecosystemLabel,
3513
+ ecosystemManifests
3514
+ ] of ecosystemGroups.entries()) {
3515
+ groups.push({ label: ecosystemLabel, manifests: ecosystemManifests });
3516
+ }
3517
+ } else {
3518
+ groups.push({ label, manifests: manifests2 });
3519
+ }
3520
+ }
3521
+ groups.sort((a, b) => {
3522
+ if (a.label.startsWith("root")) {
3523
+ if (b.label.startsWith("root")) {
3524
+ return a.label.localeCompare(b.label);
3525
+ }
3526
+ return -1;
3527
+ }
3528
+ if (b.label.startsWith("root")) {
3529
+ return 1;
3530
+ }
3531
+ return a.label.localeCompare(b.label);
3532
+ });
3533
+ return groups;
3534
+ }
3535
+
3536
+ // src/commands/project/detect.ts
3537
+ function matchManifestsWithConfig(detectedGroups, existingManifests) {
3538
+ const fileToConfig = new Map;
3539
+ for (const group of existingManifests) {
3540
+ for (const file of group.files) {
3541
+ fileToConfig.set(file, {
3542
+ label: group.label,
3543
+ allow_mix_deps: group.allow_mix_deps
3544
+ });
3545
+ }
3546
+ }
3547
+ const labelToFiles = new Map;
3548
+ const labelToConfig = new Map;
3549
+ for (const detectedGroup of detectedGroups) {
3550
+ for (const manifest of detectedGroup.manifests) {
3551
+ const existingConfig = fileToConfig.get(manifest.relativePath);
3552
+ let label;
3553
+ const hasEcosystemSuffix = detectedGroup.label.endsWith("-npm") || detectedGroup.label.endsWith("-hex") || detectedGroup.label.endsWith("-pypi");
3554
+ if (hasEcosystemSuffix) {
3555
+ label = detectedGroup.label;
3556
+ } else {
3557
+ label = existingConfig?.label ?? detectedGroup.label;
3558
+ }
3559
+ const allowMixDeps = existingConfig?.allow_mix_deps;
3560
+ if (!labelToConfig.has(label)) {
3561
+ labelToConfig.set(label, {
3562
+ files: new Set,
3563
+ allow_mix_deps: allowMixDeps
3564
+ });
3565
+ }
3566
+ const config = labelToConfig.get(label);
3567
+ config.files.add(manifest.relativePath);
3568
+ if (allowMixDeps) {
3569
+ config.allow_mix_deps = true;
3570
+ }
3571
+ }
3572
+ }
3573
+ const result = [];
3574
+ for (const [label, config] of labelToConfig.entries()) {
3575
+ const fileArray = Array.from(config.files);
3576
+ const hasNewFiles = fileArray.some((file) => !fileToConfig.has(file));
3577
+ const hasChangedLabels = fileArray.some((file) => {
3578
+ const oldConfig = fileToConfig.get(file);
3579
+ return oldConfig !== undefined && oldConfig.label !== label;
3580
+ });
3581
+ result.push({
3582
+ label,
3583
+ files: fileArray.sort(),
3584
+ isNew: hasNewFiles,
3585
+ changedLabel: hasChangedLabels ? label : undefined,
3586
+ allow_mix_deps: config.allow_mix_deps
3587
+ });
3588
+ }
3589
+ return result.sort((a, b) => {
3590
+ if (a.label.startsWith("root")) {
3591
+ if (b.label.startsWith("root")) {
3592
+ return a.label.localeCompare(b.label);
3593
+ }
3594
+ return -1;
3595
+ }
3596
+ if (b.label.startsWith("root")) {
3597
+ return 1;
3598
+ }
3599
+ return a.label.localeCompare(b.label);
3600
+ });
3601
+ }
3602
+ async function projectDetectAction(options, deps) {
3603
+ const { configService, fileSystemService, promptService, shellService } = deps;
3604
+ const projectConfig = await configService.loadProjectConfig();
3605
+ if (!projectConfig?.config.project) {
3606
+ console.error(`✗ No project is configured in pkgseer.yml`);
3607
+ console.log(`
3608
+ To get started, run: pkgseer project init`);
3609
+ console.log(` This will create a project and detect your manifest files.`);
3610
+ process.exit(1);
3611
+ }
3612
+ const projectName = projectConfig.config.project;
3613
+ const existingManifests = projectConfig.config.manifests ?? [];
3614
+ console.log(`Scanning for manifest files in project "${projectName}"...
3615
+ `);
3616
+ const cwd = fileSystemService.getCwd();
3617
+ const detectedGroups = await detectAndGroupManifests(cwd, fileSystemService, {
3618
+ maxDepth: options.maxDepth ?? 3
3619
+ });
3620
+ if (detectedGroups.length === 0) {
3621
+ console.log("No manifest files were found in the current directory.");
3622
+ if (existingManifests.length > 0) {
3623
+ console.log(`
3624
+ Your existing configuration in pkgseer.yml will remain unchanged.`);
3625
+ console.log(` Tip: Make sure you're running this command from the project root directory.`);
3626
+ } else {
3627
+ console.log(`
3628
+ Tip: Manifest files like package.json, requirements.txt, or pyproject.toml should be in the current directory or subdirectories.`);
3629
+ }
3630
+ return;
3631
+ }
3632
+ const suggestedManifests = matchManifestsWithConfig(detectedGroups, existingManifests);
3633
+ console.log("Current configuration in pkgseer.yml:");
3634
+ if (existingManifests.length === 0) {
3635
+ console.log(" (no manifests configured yet)");
3636
+ } else {
3637
+ for (const group of existingManifests) {
3638
+ console.log(` Label: ${group.label}`);
3639
+ for (const file of group.files) {
3640
+ console.log(` ${file}`);
3641
+ }
3642
+ }
3643
+ }
3644
+ console.log(`
3645
+ Suggested configuration:`);
3646
+ for (const group of suggestedManifests) {
3647
+ const markers = [];
3648
+ if (group.isNew)
3649
+ markers.push("new");
3650
+ if (group.changedLabel)
3651
+ markers.push("label changed");
3652
+ const markerStr = markers.length > 0 ? ` (${markers.join(", ")})` : "";
3653
+ console.log(` Label: ${group.label}${markerStr}`);
3654
+ for (const file of group.files) {
3655
+ const wasInConfig = existingManifests.some((g) => g.files.includes(file));
3656
+ const prefix = wasInConfig ? " " : " + ";
3657
+ console.log(`${prefix}${file}`);
3658
+ }
3659
+ }
3660
+ const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
3661
+ const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
3662
+ let allowMixDeps = false;
3663
+ if (hasHexInSuggested) {
3664
+ const existingHasMixDeps = existingManifests.some((g) => g.allow_mix_deps === true);
3665
+ if (!existingHasMixDeps) {
3666
+ console.log(`
3667
+ Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`);
3668
+ console.log(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`);
3669
+ allowMixDeps = await promptService.confirm(`
3670
+ Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
3671
+ } else {
3672
+ allowMixDeps = true;
3673
+ }
3674
+ }
3675
+ const finalManifests = suggestedManifests.map((g) => {
3676
+ const hasHexInGroup = g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
3677
+ return {
3678
+ label: g.label,
3679
+ files: g.files,
3680
+ isNew: g.isNew,
3681
+ changedLabel: g.changedLabel,
3682
+ ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {},
3683
+ ...g.allow_mix_deps !== undefined ? { allow_mix_deps: g.allow_mix_deps } : {}
3684
+ };
3685
+ });
3686
+ const hasChanges = finalManifests.length !== existingManifests.length || finalManifests.some((g) => g.isNew || g.changedLabel) || existingManifests.some((existing) => {
3687
+ const suggested = finalManifests.find((s) => s.label === existing.label);
3688
+ if (!suggested)
3689
+ return true;
3690
+ const existingFiles = new Set(existing.files);
3691
+ const suggestedFiles = new Set(suggested.files);
3692
+ return existingFiles.size !== suggestedFiles.size || !Array.from(existingFiles).every((f) => suggestedFiles.has(f));
3693
+ }) || finalManifests.some((g) => {
3694
+ const existing = existingManifests.find((e) => e.label === g.label);
3695
+ return existing?.allow_mix_deps !== g.allow_mix_deps;
3696
+ });
3697
+ if (!hasChanges) {
3698
+ console.log(`
3699
+ ✓ Your configuration is already up to date!`);
3700
+ console.log(`
3701
+ All detected manifest files match your current pkgseer.yml configuration.`);
3702
+ return;
3703
+ }
3704
+ if (options.update) {
3705
+ await configService.writeProjectConfig({
3706
+ project: projectName,
3707
+ manifests: finalManifests.map((g) => ({
3708
+ label: g.label,
3709
+ files: g.files,
3710
+ ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
3711
+ }))
3712
+ });
3713
+ console.log(`
3714
+ ✓ Configuration updated successfully!`);
3715
+ console.log(`
3716
+ Your pkgseer.yml has been updated with the detected manifest files.`);
3717
+ console.log(` Run 'pkgseer project upload' to upload them to your project.`);
3718
+ } else {
3719
+ const shouldUpdate = await promptService.confirm(`
3720
+ Would you like to update pkgseer.yml with these changes?`, false);
3721
+ if (shouldUpdate) {
3722
+ await configService.writeProjectConfig({
3723
+ project: projectName,
3724
+ manifests: finalManifests.map((g) => ({
3725
+ label: g.label,
3726
+ files: g.files,
3727
+ ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
3728
+ }))
900
3729
  });
3730
+ console.log(`
3731
+ ✓ Configuration updated successfully!`);
3732
+ console.log(`
3733
+ Your pkgseer.yml has been updated. Run 'pkgseer project upload' to upload the manifests.`);
3734
+ } else {
3735
+ console.log(`
3736
+ Configuration was not updated.`);
3737
+ console.log(` To apply these changes automatically, run: pkgseer project detect --update`);
3738
+ console.log(` Or manually edit pkgseer.yml and then run: pkgseer project upload`);
901
3739
  }
902
- };
3740
+ }
903
3741
  }
904
- // src/tools/package-dependencies.ts
905
- import { z as z2 } from "zod";
906
- var argsSchema3 = {
907
- registry: schemas.registry,
908
- package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
909
- version: schemas.version,
910
- include_transitive: z2.boolean().optional().describe("Whether to include transitive dependency DAG"),
911
- max_depth: z2.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
3742
+ var DETECT_DESCRIPTION = `Detect manifest files and update your project configuration.
3743
+
3744
+ This command scans your project directory for manifest files (like
3745
+ package.json, requirements.txt, etc.) and compares them with your
3746
+ current pkgseer.yml configuration. It will:
3747
+
3748
+ Preserve existing labels for files you've already configured
3749
+ Suggest new labels for newly detected files
3750
+ • Show you what would change before updating
3751
+
3752
+ Perfect for when you add new manifest files or reorganize your
3753
+ project structure. Run with --update to automatically apply changes.`;
3754
+ function registerProjectDetectCommand(program) {
3755
+ 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) => {
3756
+ const deps = await createContainer();
3757
+ await projectDetectAction({ maxDepth: options.maxDepth, update: options.update }, {
3758
+ configService: deps.configService,
3759
+ fileSystemService: deps.fileSystemService,
3760
+ promptService: deps.promptService,
3761
+ shellService: deps.shellService
3762
+ });
3763
+ });
3764
+ }
3765
+ // src/commands/shared-colors.ts
3766
+ var colors2 = {
3767
+ reset: "\x1B[0m",
3768
+ bold: "\x1B[1m",
3769
+ dim: "\x1B[2m",
3770
+ green: "\x1B[32m",
3771
+ yellow: "\x1B[33m",
3772
+ blue: "\x1B[34m",
3773
+ magenta: "\x1B[35m",
3774
+ cyan: "\x1B[36m",
3775
+ red: "\x1B[31m"
912
3776
  };
913
- function createPackageDependenciesTool(pkgseerService) {
914
- return {
915
- name: "package_dependencies",
916
- description: "Retrieves direct and transitive dependencies for a package version",
917
- schema: argsSchema3,
918
- handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
919
- return withErrorHandling("fetch package dependencies", async () => {
920
- const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry(registry), package_name, version2, include_transitive, max_depth);
921
- const graphqlError = handleGraphQLErrors(result.errors);
922
- if (graphqlError)
923
- return graphqlError;
924
- if (!result.data.packageDependencies) {
925
- return notFoundError(package_name, registry);
3777
+ function shouldUseColors2(noColor) {
3778
+ if (noColor)
3779
+ return false;
3780
+ if (process.env.NO_COLOR !== undefined)
3781
+ return false;
3782
+ return process.stdout.isTTY ?? false;
3783
+ }
3784
+ function success(text, useColors) {
3785
+ const checkmark = useColors ? `${colors2.green}✓${colors2.reset}` : "✓";
3786
+ return `${checkmark} ${text}`;
3787
+ }
3788
+ function error(text, useColors) {
3789
+ const cross = useColors ? `${colors2.red}✗${colors2.reset}` : "✗";
3790
+ return `${cross} ${text}`;
3791
+ }
3792
+ function highlight(text, useColors) {
3793
+ if (!useColors)
3794
+ return text;
3795
+ return `${colors2.bold}${colors2.cyan}${text}${colors2.reset}`;
3796
+ }
3797
+ function dim(text, useColors) {
3798
+ if (!useColors)
3799
+ return text;
3800
+ return `${colors2.dim}${text}${colors2.reset}`;
3801
+ }
3802
+
3803
+ // src/commands/project/manifest-upload-utils.ts
3804
+ async function processManifestFiles(params) {
3805
+ const {
3806
+ files,
3807
+ basePath,
3808
+ hasHexManifests,
3809
+ allowMixDeps,
3810
+ fileSystemService,
3811
+ shellService
3812
+ } = params;
3813
+ const hexFiles = files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
3814
+ let generatedHexFiles = [];
3815
+ if (hasHexManifests && allowMixDeps && hexFiles.length > 0) {
3816
+ const firstHexFile = hexFiles[0];
3817
+ if (!firstHexFile) {
3818
+ throw new Error("No hex files found");
3819
+ }
3820
+ const absolutePath = fileSystemService.joinPath(basePath, firstHexFile);
3821
+ const manifestDir = fileSystemService.getDirname(absolutePath);
3822
+ try {
3823
+ const [depsContent, depsTreeContent] = await Promise.all([
3824
+ shellService.execute("mix deps --all", manifestDir),
3825
+ shellService.execute("mix deps.tree", manifestDir)
3826
+ ]);
3827
+ generatedHexFiles = [
3828
+ {
3829
+ filename: "deps.txt",
3830
+ path: absolutePath,
3831
+ content: depsContent
3832
+ },
3833
+ {
3834
+ filename: "deps-tree.txt",
3835
+ path: absolutePath,
3836
+ content: depsTreeContent
926
3837
  }
927
- return textResult(JSON.stringify(result.data.packageDependencies, null, 2));
928
- });
3838
+ ];
3839
+ } catch (error2) {
3840
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
3841
+ throw new Error(`Failed to generate dependencies for hex manifest: ${firstHexFile}
3842
+ ` + ` Error: ${errorMessage}
3843
+ ` + ` Make sure 'mix' is installed and the directory contains a valid Elixir project.`);
929
3844
  }
930
- };
3845
+ }
3846
+ const filePromises = files.map(async (relativePath) => {
3847
+ const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
3848
+ if (isHexFile) {
3849
+ if (!allowMixDeps) {
3850
+ return null;
3851
+ }
3852
+ return null;
3853
+ }
3854
+ const absolutePath = fileSystemService.joinPath(basePath, relativePath);
3855
+ const exists = await fileSystemService.exists(absolutePath);
3856
+ if (!exists) {
3857
+ throw new Error(`File not found: ${relativePath}
3858
+ Make sure the file exists and the path in pkgseer.yml is correct.`);
3859
+ }
3860
+ const content = await fileSystemService.readFile(absolutePath);
3861
+ const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
3862
+ return {
3863
+ filename,
3864
+ path: absolutePath,
3865
+ content
3866
+ };
3867
+ });
3868
+ const regularFiles = await Promise.all(filePromises);
3869
+ const result = [];
3870
+ let hexFilesInserted = false;
3871
+ for (let i = 0;i < files.length; i++) {
3872
+ const relativePath = files[i];
3873
+ if (!relativePath) {
3874
+ continue;
3875
+ }
3876
+ const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
3877
+ if (isHexFile && !hexFilesInserted && generatedHexFiles.length > 0) {
3878
+ result.push(...generatedHexFiles);
3879
+ hexFilesInserted = true;
3880
+ } else if (!isHexFile) {
3881
+ const regularFile = regularFiles[i];
3882
+ if (regularFile !== undefined) {
3883
+ result.push(regularFile);
3884
+ }
3885
+ } else {
3886
+ result.push(null);
3887
+ }
3888
+ }
3889
+ return result;
931
3890
  }
932
- // src/tools/package-quality.ts
933
- var argsSchema4 = {
934
- registry: schemas.registry,
935
- package_name: schemas.packageName.describe("Name of the package to analyze"),
936
- version: schemas.version
937
- };
938
- function createPackageQualityTool(pkgseerService) {
939
- return {
940
- name: "package_quality",
941
- description: "Retrieves quality score and rule-level breakdown for a package",
942
- schema: argsSchema4,
943
- handler: async ({ registry, package_name, version: version2 }, _extra) => {
944
- return withErrorHandling("fetch package quality", async () => {
945
- const result = await pkgseerService.getPackageQuality(toGraphQLRegistry(registry), package_name, version2);
946
- const graphqlError = handleGraphQLErrors(result.errors);
947
- if (graphqlError)
948
- return graphqlError;
949
- if (!result.data.packageQuality) {
950
- return notFoundError(package_name, registry);
951
- }
952
- return textResult(JSON.stringify(result.data.packageQuality, null, 2));
953
- });
3891
+
3892
+ // src/commands/project/init.ts
3893
+ async function projectInitAction(options, deps) {
3894
+ const {
3895
+ projectService,
3896
+ configService,
3897
+ fileSystemService,
3898
+ gitService,
3899
+ promptService,
3900
+ shellService,
3901
+ authStorage,
3902
+ baseUrl
3903
+ } = deps;
3904
+ const useColors = shouldUseColors2();
3905
+ const auth = await checkProjectWriteScope(authStorage, baseUrl);
3906
+ if (!auth) {
3907
+ console.error(error(`Authentication required with ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope`, useColors));
3908
+ console.log(`
3909
+ Your current token doesn't have the required permissions for creating projects and uploading manifests.`);
3910
+ console.log(`
3911
+ To fix this:`);
3912
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3913
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3914
+ console.log(`
3915
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3916
+ process.exit(1);
3917
+ }
3918
+ const isEnvToken = auth.scopes.length === 0 && auth.tokenName === "PKGSEER_API_TOKEN";
3919
+ if (isEnvToken) {} else if (!auth.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
3920
+ console.error(error(`Token missing required scope: ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)}`, useColors));
3921
+ console.log(`
3922
+ To fix this:`);
3923
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3924
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3925
+ console.log(`
3926
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3927
+ process.exit(1);
3928
+ }
3929
+ const existingConfig = await configService.loadProjectConfig();
3930
+ if (existingConfig?.config.project) {
3931
+ console.error(error(`A project is already configured in pkgseer.yml: ${highlight(existingConfig.config.project, useColors)}`, useColors));
3932
+ console.log(dim(`
3933
+ To reinitialize, either remove pkgseer.yml or edit it manually.`, useColors));
3934
+ console.log(dim(` To update manifest files, use: `, useColors) + highlight(`pkgseer project detect`, useColors));
3935
+ process.exit(1);
3936
+ }
3937
+ let projectName = options.name?.trim();
3938
+ if (!projectName) {
3939
+ const cwd2 = fileSystemService.getCwd();
3940
+ const basename = cwd2.split(/[/\\]/).pop() ?? "project";
3941
+ const input2 = await promptService.input(`Project name:`, basename);
3942
+ projectName = input2.trim();
3943
+ }
3944
+ console.log(`
3945
+ Creating project ${highlight(projectName, useColors)}...`);
3946
+ let createResult;
3947
+ let projectAlreadyExists = false;
3948
+ try {
3949
+ createResult = await projectService.createProject({
3950
+ name: projectName
3951
+ });
3952
+ } catch (createError) {
3953
+ const errorMessage = createError instanceof Error ? createError.message : String(createError);
3954
+ if (createError instanceof Error && (errorMessage.includes("already been taken") || errorMessage.includes("already exists"))) {
3955
+ console.log(dim(`
3956
+ Project ${highlight(projectName, useColors)} already exists on the server.`, useColors));
3957
+ console.log(dim(` This might happen if you previously ran init but didn't complete the setup.`, useColors));
3958
+ const useExisting = await promptService.confirm(`
3959
+ Do you want to use the existing project and continue with manifest setup?`, true);
3960
+ if (!useExisting) {
3961
+ console.log(dim(`
3962
+ Exiting. Please choose a different project name or use an existing project.`, useColors));
3963
+ process.exit(0);
3964
+ }
3965
+ projectAlreadyExists = true;
3966
+ createResult = {
3967
+ project: {
3968
+ name: projectName,
3969
+ defaultBranch: "main"
3970
+ },
3971
+ errors: null
3972
+ };
3973
+ } else {
3974
+ console.error(error(`Failed to create project: ${errorMessage}`, useColors));
3975
+ if (createError instanceof Error && (errorMessage.includes("Insufficient permissions") || errorMessage.includes("Required scopes") || errorMessage.includes(PROJECT_MANIFEST_UPLOAD_SCOPE))) {
3976
+ console.log(`
3977
+ Your current token doesn't have the required permissions for creating projects.`);
3978
+ console.log(`
3979
+ To fix this:`);
3980
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3981
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3982
+ console.log(`
3983
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3984
+ } else if (createError instanceof Error && (errorMessage.includes("alphanumeric") || errorMessage.includes("hyphens") || errorMessage.includes("underscores"))) {
3985
+ console.log(dim(`
3986
+ Project name requirements:`, useColors));
3987
+ console.log(dim(` • Must start with an alphanumeric character (a-z, A-Z, 0-9)`, useColors));
3988
+ console.log(dim(` • Can contain letters, numbers, hyphens (-), and underscores (_)`, useColors));
3989
+ console.log(dim(` • Example valid names: ${highlight(`my-project`, useColors)}, ${highlight(`project_123`, useColors)}, ${highlight(`backend`, useColors)}`, useColors));
3990
+ }
3991
+ process.exit(1);
954
3992
  }
3993
+ }
3994
+ if (!createResult.project) {
3995
+ if (createResult.errors && createResult.errors.length > 0) {
3996
+ const firstError = createResult.errors[0];
3997
+ console.error(error(`Failed to create project: ${firstError?.message ?? "Unknown error"}`, useColors));
3998
+ if (firstError?.field) {
3999
+ console.log(dim(`
4000
+ Field: ${firstError.field}`, useColors));
4001
+ }
4002
+ process.exit(1);
4003
+ }
4004
+ console.error(error(`Failed to create project`, useColors));
4005
+ console.log(dim(`
4006
+ Please try again or contact support if the issue persists.`, useColors));
4007
+ process.exit(1);
4008
+ }
4009
+ if (projectAlreadyExists) {
4010
+ console.log(success(`Using existing project ${highlight(createResult.project.name, useColors)}`, useColors));
4011
+ } else {
4012
+ console.log(success(`Project ${highlight(createResult.project.name, useColors)} created successfully!`, useColors));
4013
+ }
4014
+ const maxDepth = options.maxDepth ?? 3;
4015
+ console.log(dim(`
4016
+ Scanning for manifest files (max depth: ${maxDepth})...`, useColors));
4017
+ const cwd = fileSystemService.getCwd();
4018
+ const manifestGroups = await detectAndGroupManifests(cwd, fileSystemService, {
4019
+ maxDepth
4020
+ });
4021
+ const configToWrite = {
4022
+ project: projectName
955
4023
  };
956
- }
957
- // src/tools/compare-packages.ts
958
- import { z as z3 } from "zod";
959
- var packageInputSchema = z3.object({
960
- registry: z3.enum(["npm", "pypi", "hex"]),
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)")
966
- };
967
- function createComparePackagesTool(pkgseerService) {
968
- return {
969
- name: "compare_packages",
970
- description: "Compares multiple packages across metadata, quality, and security dimensions",
971
- schema: argsSchema5,
972
- handler: async ({ packages }, _extra) => {
973
- return withErrorHandling("compare packages", async () => {
974
- const input = packages.map((pkg) => ({
975
- registry: toGraphQLRegistry(pkg.registry),
976
- name: pkg.name,
977
- version: pkg.version
978
- }));
979
- const result = await pkgseerService.comparePackages(input);
980
- const graphqlError = handleGraphQLErrors(result.errors);
981
- if (graphqlError)
982
- return graphqlError;
983
- if (!result.data.comparePackages) {
984
- return errorResult("Comparison failed: no results returned");
4024
+ if (manifestGroups.length > 0) {
4025
+ console.log(`
4026
+ Found ${highlight(`${manifestGroups.length}`, useColors)} manifest group${manifestGroups.length === 1 ? "" : "s"}:
4027
+ `);
4028
+ for (const group of manifestGroups) {
4029
+ console.log(` Label: ${highlight(group.label, useColors)}`);
4030
+ for (const manifest of group.manifests) {
4031
+ console.log(` ${highlight(manifest.relativePath, useColors)} ${dim(`(${manifest.type})`, useColors)}`);
4032
+ }
4033
+ }
4034
+ const hasHexManifests = manifestGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
4035
+ let allowMixDeps = false;
4036
+ if (hasHexManifests) {
4037
+ console.log(dim(`
4038
+ Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
4039
+ console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
4040
+ allowMixDeps = await promptService.confirm(`
4041
+ Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
4042
+ }
4043
+ configToWrite.manifests = manifestGroups.map((group) => {
4044
+ const hasHexInGroup = group.manifests.some((m) => m.type === "hex");
4045
+ return {
4046
+ label: group.label,
4047
+ files: group.manifests.map((m) => m.relativePath),
4048
+ ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {}
4049
+ };
4050
+ });
4051
+ const action = await promptService.select(`
4052
+ What would you like to do next?`, [
4053
+ {
4054
+ value: "upload",
4055
+ name: "Save config and upload manifests now",
4056
+ description: "Recommended: saves configuration and uploads files immediately"
4057
+ },
4058
+ {
4059
+ value: "edit",
4060
+ name: "Save config for manual editing",
4061
+ description: "Saves configuration so you can customize labels before uploading"
4062
+ },
4063
+ {
4064
+ value: "abort",
4065
+ name: "Skip configuration for now",
4066
+ description: "Project is created but no config saved (you can run init again later)"
4067
+ }
4068
+ ]);
4069
+ if (action === "abort") {
4070
+ if (projectAlreadyExists) {
4071
+ console.log(`
4072
+ ${success(`Using existing project ${highlight(projectName, useColors)}`, useColors)}`);
4073
+ } else {
4074
+ console.log(`
4075
+ ${success(`Project ${highlight(projectName, useColors)} created successfully!`, useColors)}`);
4076
+ }
4077
+ console.log(dim(`
4078
+ Configuration was not saved. To configure later, run:`, useColors));
4079
+ console.log(` ${highlight(`pkgseer project init --name ${projectName}`, useColors)}`);
4080
+ console.log(dim(`
4081
+ Or manually create pkgseer.yml and run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4082
+ process.exit(0);
4083
+ }
4084
+ if (action === "upload") {
4085
+ await configService.writeProjectConfig(configToWrite);
4086
+ console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
4087
+ const branch = await gitService.getCurrentBranch() ?? createResult.project.defaultBranch;
4088
+ console.log(`
4089
+ Uploading manifest files to branch ${highlight(branch, useColors)}...`);
4090
+ const cwd2 = fileSystemService.getCwd();
4091
+ for (const group of manifestGroups) {
4092
+ try {
4093
+ const hasHexManifests2 = group.manifests.some((m) => m.type === "hex");
4094
+ const allowMixDeps2 = configToWrite.manifests?.find((m) => m.label === group.label)?.allow_mix_deps === true;
4095
+ const allFiles = await processManifestFiles({
4096
+ files: group.manifests.map((m) => m.relativePath),
4097
+ basePath: cwd2,
4098
+ hasHexManifests: hasHexManifests2,
4099
+ allowMixDeps: allowMixDeps2 ?? false,
4100
+ fileSystemService,
4101
+ shellService
4102
+ });
4103
+ const validFiles = allFiles.filter((f) => f !== null);
4104
+ if (validFiles.length === 0) {
4105
+ console.log(` ${dim(`(no files to upload for ${group.label})`, useColors)}`);
4106
+ continue;
4107
+ }
4108
+ const uploadResult = await projectService.uploadManifests({
4109
+ project: projectName,
4110
+ branch,
4111
+ label: group.label,
4112
+ files: validFiles
4113
+ });
4114
+ for (const result of uploadResult.results) {
4115
+ if (result.status === "success") {
4116
+ const depsCount = result.dependencies_count ?? 0;
4117
+ const labelText = highlight(group.label, useColors);
4118
+ const fileText = highlight(result.filename, useColors);
4119
+ const depsText = dim(`(${depsCount} dependencies)`, useColors);
4120
+ console.log(` ${success(`${labelText}: ${fileText}`, useColors)} ${depsText}`);
4121
+ } else {
4122
+ console.log(` ${error(`${group.label}: ${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
4123
+ }
4124
+ }
4125
+ } catch (uploadError) {
4126
+ const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
4127
+ let userMessage = `Failed to upload ${group.label}: ${errorMessage}`;
4128
+ if (hasHexManifests) {
4129
+ if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
4130
+ userMessage = `Failed to process hex manifest files for ${group.label}.
4131
+ ` + ` Error: ${errorMessage}
4132
+ ` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
4133
+ } else if (errorMessage.includes("Failed to generate dependencies")) {
4134
+ userMessage = errorMessage;
4135
+ } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
4136
+ userMessage = `Failed to upload ${group.label}: Network error.
4137
+ ` + ` Error: ${errorMessage}
4138
+ ` + ` Check your internet connection and try again.`;
4139
+ }
4140
+ } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
4141
+ userMessage = `Failed to upload ${group.label}: Network error.
4142
+ ` + ` Error: ${errorMessage}
4143
+ ` + ` Check your internet connection and try again.`;
4144
+ }
4145
+ console.error(error(userMessage, useColors));
985
4146
  }
986
- return textResult(JSON.stringify(result.data.comparePackages, null, 2));
987
- });
4147
+ }
4148
+ console.log(`
4149
+ ${success(`Project initialization complete!`, useColors)}`);
4150
+ console.log(dim(`
4151
+ View your project: `, useColors) + highlight(`${deps.baseUrl}/projects/${projectName}`, useColors));
4152
+ console.log(dim(`
4153
+ To upload updated manifests later, run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4154
+ return;
988
4155
  }
989
- };
4156
+ } else {
4157
+ console.log(dim(`
4158
+ No manifest files were found in the current directory.`, useColors));
4159
+ }
4160
+ await configService.writeProjectConfig(configToWrite);
4161
+ console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
4162
+ if (manifestGroups.length > 0) {
4163
+ console.log(dim(`
4164
+ Next steps:`, useColors));
4165
+ console.log(dim(` 1. Edit pkgseer.yml to customize manifest labels if needed`, useColors));
4166
+ console.log(dim(` 2. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4167
+ console.log(dim(`
4168
+ Tip: Use `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` to automatically update your config when you add new manifest files.`, useColors));
4169
+ } else {
4170
+ console.log(dim(`
4171
+ Next steps:`, useColors));
4172
+ console.log(dim(` 1. Add manifest files to your project`, useColors));
4173
+ console.log(dim(` 2. Edit pkgseer.yml to configure them:`, useColors));
4174
+ console.log(dim(`
4175
+ manifests:`, useColors));
4176
+ console.log(dim(` - label: backend`, useColors));
4177
+ console.log(dim(` files:`, useColors));
4178
+ console.log(dim(` - `, useColors) + highlight(`package-lock.json`, useColors));
4179
+ console.log(dim(`
4180
+ 3. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4181
+ console.log(dim(`
4182
+ Tip: Run `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` after adding manifest files to auto-configure them.`, useColors));
4183
+ }
990
4184
  }
991
- // src/commands/mcp.ts
992
- function createMcpServer(deps) {
993
- const { pkgseerService } = deps;
994
- const server = new McpServer({
995
- name: "pkgseer",
996
- version: "0.1.0"
4185
+ var INIT_DESCRIPTION = `Initialize a new project in the current directory.
4186
+
4187
+ This command will:
4188
+ 1. Create a new project entry (or prompt for a project name)
4189
+ 2. Scan for manifest files (package.json, requirements.txt, etc.)
4190
+ 3. Suggest labels based on directory structure
4191
+ 4. Optionally upload manifests to the project
4192
+
4193
+ The project name defaults to the current directory name, or you can
4194
+ specify it with --name. Manifest files are automatically detected
4195
+ and grouped by their directory structure.`;
4196
+ function registerProjectInitCommand(program) {
4197
+ 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) => {
4198
+ const deps = await createContainer();
4199
+ await projectInitAction({ name: options.name, maxDepth: options.maxDepth }, {
4200
+ projectService: deps.projectService,
4201
+ configService: deps.configService,
4202
+ fileSystemService: deps.fileSystemService,
4203
+ gitService: deps.gitService,
4204
+ promptService: deps.promptService,
4205
+ shellService: deps.shellService,
4206
+ authStorage: deps.authStorage,
4207
+ baseUrl: deps.baseUrl
4208
+ });
997
4209
  });
998
- const tools = [
999
- createPackageSummaryTool(pkgseerService),
1000
- createPackageVulnerabilitiesTool(pkgseerService),
1001
- createPackageDependenciesTool(pkgseerService),
1002
- createPackageQualityTool(pkgseerService),
1003
- createComparePackagesTool(pkgseerService)
1004
- ];
1005
- for (const tool of tools) {
1006
- server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
1007
- }
1008
- return server;
1009
4210
  }
1010
- function requireAuth(deps) {
1011
- if (deps.hasValidToken) {
1012
- return;
4211
+ // src/commands/project/upload.ts
4212
+ async function projectUploadAction(options, deps) {
4213
+ const {
4214
+ projectService,
4215
+ configService,
4216
+ fileSystemService,
4217
+ gitService,
4218
+ shellService,
4219
+ promptService,
4220
+ authStorage,
4221
+ baseUrl
4222
+ } = deps;
4223
+ const useColors = shouldUseColors2();
4224
+ const tokenData = await checkProjectWriteScope(authStorage, baseUrl);
4225
+ if (!tokenData) {
4226
+ console.error(error(`Authentication required. Please run 'pkgseer login'.`, useColors));
4227
+ console.log(`
4228
+ To fix this:`);
4229
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
4230
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
4231
+ console.log(`
4232
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
4233
+ process.exit(1);
1013
4234
  }
1014
- console.log(`Authentication required to start MCP server.
1015
- `);
1016
- if (deps.baseUrl !== "https://pkgseer.dev") {
1017
- console.log(` Environment: ${deps.baseUrl}`);
1018
- console.log(` You're using a custom environment.
1019
- `);
4235
+ const isEnvToken = tokenData.scopes.length === 0 && tokenData.tokenName === "PKGSEER_API_TOKEN";
4236
+ if (isEnvToken) {} else if (!tokenData.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
4237
+ console.error(error(`Token missing required scope: ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)}`, useColors));
4238
+ console.log(`
4239
+ To fix this:`);
4240
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
4241
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
4242
+ console.log(`
4243
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
4244
+ process.exit(1);
1020
4245
  }
1021
- console.log("To authenticate:");
1022
- console.log(` pkgseer login
4246
+ const projectConfig = await configService.loadProjectConfig();
4247
+ if (!projectConfig?.config.project) {
4248
+ console.error(error(`No project is configured in pkgseer.yml`, useColors));
4249
+ console.log(`
4250
+ To get started, run: ${highlight(`pkgseer project init`, useColors)}`);
4251
+ console.log(` This will create a project and detect your manifest files.`);
4252
+ process.exit(1);
4253
+ }
4254
+ const projectName = projectConfig.config.project;
4255
+ let manifests = projectConfig.config.manifests ?? [];
4256
+ const configDir = fileSystemService.getDirname(projectConfig.path);
4257
+ if (manifests.length === 0) {
4258
+ console.error(error(`No manifest files are configured in pkgseer.yml`, useColors));
4259
+ console.log(`
4260
+ To add manifest files, you can:`);
4261
+ console.log(` 1. Run: ${highlight(`pkgseer project detect`, useColors)}`);
4262
+ console.log(` This will automatically detect and configure manifest files`);
4263
+ console.log(` 2. Or manually edit pkgseer.yml to add manifest files`);
4264
+ console.log(`
4265
+ Then run this command again to upload them.`);
4266
+ process.exit(1);
4267
+ }
4268
+ const needsPermission = manifests.some((group) => {
4269
+ const hasHexFiles = group.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
4270
+ return hasHexFiles && group.allow_mix_deps !== true;
4271
+ });
4272
+ if (needsPermission) {
4273
+ console.log(dim(`
4274
+ Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
4275
+ console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
4276
+ const allowMixDeps = await promptService.confirm(`
4277
+ Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
4278
+ if (!allowMixDeps) {
4279
+ console.log(dim(`
4280
+ Upload cancelled. Hex manifest files cannot be uploaded directly.`, useColors));
4281
+ console.log(dim(` To upload hex manifests, you must allow running "mix deps --all" and "mix deps.tree".`, useColors));
4282
+ process.exit(0);
4283
+ }
4284
+ manifests = manifests.map((group) => {
4285
+ const hasHexFiles = group.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
4286
+ if (hasHexFiles) {
4287
+ return { ...group, allow_mix_deps: true };
4288
+ }
4289
+ return group;
4290
+ });
4291
+ await configService.writeProjectConfig({
4292
+ project: projectName,
4293
+ manifests
4294
+ });
4295
+ console.log(success(`Configuration updated: ${highlight("allow_mix_deps", useColors)} permission saved`, useColors));
4296
+ }
4297
+ let branch = options.branch;
4298
+ if (!branch) {
4299
+ branch = await gitService.getCurrentBranch() ?? "main";
4300
+ }
4301
+ console.log(`Uploading manifest files to project ${highlight(projectName, useColors)}`);
4302
+ console.log(`Branch: ${highlight(branch, useColors)}
1023
4303
  `);
1024
- console.log("Or set PKGSEER_API_TOKEN environment variable.");
1025
- process.exit(1);
4304
+ const basePath = configDir;
4305
+ let totalSucceeded = 0;
4306
+ let totalFailed = 0;
4307
+ for (const manifestGroup of manifests) {
4308
+ console.log(`Uploading ${manifestGroup.label}...`);
4309
+ try {
4310
+ const hexFiles = manifestGroup.files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
4311
+ const hasHexManifests = hexFiles.length > 0;
4312
+ const isHexGroup = manifestGroup.allow_mix_deps === true;
4313
+ if (hasHexManifests && !isHexGroup) {
4314
+ console.log(` ${dim(`Skipping hex files. Please run 'pkgseer project detect' to update configuration.`, useColors)}`);
4315
+ totalFailed += hexFiles.length;
4316
+ continue;
4317
+ }
4318
+ let allFiles;
4319
+ try {
4320
+ allFiles = await processManifestFiles({
4321
+ files: manifestGroup.files,
4322
+ basePath,
4323
+ hasHexManifests,
4324
+ allowMixDeps: isHexGroup,
4325
+ fileSystemService,
4326
+ shellService
4327
+ });
4328
+ } catch (processError) {
4329
+ const errorMessage = processError instanceof Error ? processError.message : String(processError);
4330
+ let userMessage = `Failed to process files for ${manifestGroup.label}: ${errorMessage}`;
4331
+ if (hasHexManifests) {
4332
+ if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
4333
+ userMessage = `Failed to process hex manifest files for ${manifestGroup.label}.
4334
+ ` + ` Error: ${errorMessage}
4335
+ ` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
4336
+ } else if (errorMessage.includes("Failed to generate dependencies")) {
4337
+ userMessage = errorMessage;
4338
+ } else {
4339
+ userMessage = `Failed to process hex manifest files for ${manifestGroup.label}.
4340
+ ` + ` Error: ${errorMessage}
4341
+ ` + ` Make sure the directory contains a valid Elixir project and dependencies can be resolved.`;
4342
+ }
4343
+ }
4344
+ console.error(error(userMessage, useColors));
4345
+ totalFailed += manifestGroup.files.length;
4346
+ continue;
4347
+ }
4348
+ const validFiles = allFiles.filter((f) => f !== null);
4349
+ if (validFiles.length === 0) {
4350
+ console.log(` ${dim(`(no files to upload for this group)`, useColors)}`);
4351
+ continue;
4352
+ }
4353
+ const uploadResult = await projectService.uploadManifests({
4354
+ project: projectName,
4355
+ branch,
4356
+ label: manifestGroup.label,
4357
+ files: validFiles
4358
+ });
4359
+ for (const result of uploadResult.results) {
4360
+ if (result.status === "success") {
4361
+ const depsCount = result.dependencies_count ?? 0;
4362
+ console.log(` ${success(`${highlight(result.filename, useColors)}`, useColors)} ${dim(`(${depsCount} dependencies)`, useColors)}`);
4363
+ totalSucceeded++;
4364
+ } else {
4365
+ console.log(` ${error(`${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
4366
+ totalFailed++;
4367
+ }
4368
+ }
4369
+ } catch (uploadError) {
4370
+ console.error(error(`Failed to upload ${manifestGroup.label}: ${uploadError instanceof Error ? uploadError.message : uploadError}`, useColors));
4371
+ totalFailed += manifestGroup.files.length;
4372
+ }
4373
+ }
4374
+ if (totalSucceeded > 0 || totalFailed > 0) {
4375
+ const successText = totalSucceeded > 0 ? `${totalSucceeded} file${totalSucceeded === 1 ? "" : "s"} succeeded` : "";
4376
+ const failText = totalFailed > 0 ? `${totalFailed} file${totalFailed === 1 ? "" : "s"} failed` : "";
4377
+ const statusText = [successText, failText].filter(Boolean).join(", ");
4378
+ console.log(`
4379
+ ${success(`Upload complete: ${statusText}`, useColors)}`);
4380
+ }
4381
+ if (totalFailed > 0) {
4382
+ console.log(`
4383
+ Some files failed to upload. Please check the errors above and try again.`);
4384
+ console.log(` Tip: Make sure all files exist and are readable, and that you're authenticated with proper scopes.`);
4385
+ console.log(` Check your scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
4386
+ process.exit(1);
4387
+ }
4388
+ console.log(`
4389
+ Your manifest files have been uploaded successfully!`);
4390
+ console.log(` View your project dependencies and insights in the PkgSeer dashboard.`);
1026
4391
  }
1027
- async function startMcpServer(deps) {
1028
- requireAuth(deps);
1029
- const server = createMcpServer(deps);
1030
- const transport = new StdioServerTransport;
1031
- await server.connect(transport);
4392
+ var UPLOAD_DESCRIPTION = `Upload manifest files to your project.
4393
+
4394
+ This command reads the manifest files configured in pkgseer.yml
4395
+ and uploads them to your project. It will:
4396
+
4397
+ • Upload each manifest group with its configured label
4398
+ • Use your current git branch (or 'main' if not in a git repo)
4399
+ • Show progress and results for each file
4400
+
4401
+ Make sure you've configured manifest files first (using 'pkgseer
4402
+ project init' or 'pkgseer project detect'), and that you're
4403
+ authenticated (run 'pkgseer login' if needed).`;
4404
+ function registerProjectUploadCommand(program) {
4405
+ 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) => {
4406
+ const deps = await createContainer();
4407
+ await projectUploadAction(options, {
4408
+ projectService: deps.projectService,
4409
+ configService: deps.configService,
4410
+ fileSystemService: deps.fileSystemService,
4411
+ gitService: deps.gitService,
4412
+ shellService: deps.shellService,
4413
+ promptService: deps.promptService,
4414
+ authStorage: deps.authStorage,
4415
+ baseUrl: deps.baseUrl
4416
+ });
4417
+ });
1032
4418
  }
1033
- function registerMcpCommand(program) {
1034
- program.command("mcp").summary("Start MCP server for AI assistants").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
4419
+ // src/commands/quickstart.ts
4420
+ var QUICKSTART_TEXT = `# PkgSeer CLI - Quick Reference for AI Agents
1035
4421
 
1036
- This allows AI assistants like Claude, Cursor, and others to query package
1037
- information directly. Add this to your assistant's MCP configuration:
4422
+ PkgSeer provides package intelligence for npm, PyPI, and Hex registries.
4423
+
4424
+ ## Package Commands (pkgseer pkg)
4425
+
4426
+ ### Get package info
4427
+ \`pkgseer pkg info <package> [-r npm|pypi|hex] [--json]\`
4428
+ Returns: metadata, versions, security advisories, quickstart
4429
+
4430
+ ### Check quality score
4431
+ \`pkgseer pkg quality <package> [-r registry] [-v pkg-version] [--json]\`
4432
+ Returns: overall score, category breakdown, rule details
4433
+
4434
+ ### List dependencies
4435
+ \`pkgseer pkg deps <package> [-r registry] [-v pkg-version] [-t] [-d depth] [--json]\`
4436
+ Returns: direct deps, transitive count, dependency tree (with -t)
4437
+
4438
+ ### Check vulnerabilities
4439
+ \`pkgseer pkg vulns <package> [-r registry] [-v pkg-version] [--json]\`
4440
+ Returns: CVEs, severity, affected versions, upgrade guidance
4441
+
4442
+ ### Compare packages
4443
+ \`pkgseer pkg compare <pkg1> <pkg2> [...] [--json]\`
4444
+ Format: [registry:]name[@version] (e.g., npm:lodash, pypi:requests@2.31.0)
4445
+ Returns: side-by-side quality, downloads, vulnerabilities
4446
+
4447
+ ## Documentation Commands (pkgseer docs)
4448
+
4449
+ ### List doc pages
4450
+ \`pkgseer docs list <package> [-r registry] [-v pkg-version] [--json]\`
4451
+ Returns: page titles, IDs (slugs), word counts
1038
4452
 
4453
+ ### Get doc page
4454
+ \`pkgseer docs get <package>/<page-id> [<package>/<page-id> ...] [-r registry] [-v pkg-version] [--json]\`
4455
+ Returns: full page content (markdown), breadcrumbs, source URL
4456
+ Supports multiple pages: \`pkgseer docs get express/readme express/api\`
4457
+
4458
+ ### Search docs
4459
+ \`pkgseer docs search "<query>" [-p package] [-r registry] [--json]\`
4460
+ \`pkgseer docs search --keywords term1,term2 [-s] [-l limit]\`
4461
+ Default: searches project docs (if project configured)
4462
+ With -p: searches specific package docs
4463
+ Returns: ranked results with relevance scores
4464
+
4465
+ ## Tips for AI Agents
4466
+
4467
+ 1. Use \`--json\` for structured data parsing
4468
+ 2. Default registry is npm; use \`-r pypi\` or \`-r hex\` for others
4469
+ 3. Version defaults to latest; use \`-v <version>\` for specific version
4470
+ 4. For docs search, configure project in pkgseer.yml for project-wide search
4471
+ 5. Compare up to 10 packages at once with \`pkg compare\`
4472
+
4473
+ ## MCP Server
4474
+
4475
+ For Model Context Protocol integration:
4476
+ \`pkgseer mcp\`
4477
+
4478
+ Add to MCP config:
4479
+ {
1039
4480
  "pkgseer": {
1040
4481
  "command": "pkgseer",
1041
4482
  "args": ["mcp"]
1042
4483
  }
4484
+ }
4485
+ `;
4486
+ function quickstartAction(options) {
4487
+ if (options.json) {
4488
+ console.log(JSON.stringify({
4489
+ version,
4490
+ quickstart: QUICKSTART_TEXT
4491
+ }));
4492
+ } else {
4493
+ console.log(QUICKSTART_TEXT);
4494
+ }
4495
+ }
4496
+ var QUICKSTART_DESCRIPTION = `Show quick reference for AI agents.
1043
4497
 
1044
- Available tools: package_summary, package_vulnerabilities,
1045
- package_dependencies, package_quality, compare_packages`).action(async () => {
1046
- const deps = await createContainer();
1047
- await startMcpServer(deps);
4498
+ Displays a concise guide to pkgseer CLI commands optimized
4499
+ for LLM agents. Includes command syntax, options, and tips
4500
+ for effective usage.
4501
+
4502
+ Use --json for structured output.`;
4503
+ function registerQuickstartCommand(program) {
4504
+ program.command("quickstart").summary("Show quick reference for AI agents").description(QUICKSTART_DESCRIPTION).option("--json", "Output as JSON").action((options) => {
4505
+ quickstartAction(options);
1048
4506
  });
1049
4507
  }
1050
4508
 
@@ -1054,11 +4512,41 @@ program.name("pkgseer").description("Package intelligence for your AI assistant"
1054
4512
  Getting started:
1055
4513
  pkgseer login Authenticate with your account
1056
4514
  pkgseer mcp Start the MCP server for AI assistants
4515
+ pkgseer quickstart Show quick reference for AI agents
4516
+
4517
+ Package commands:
4518
+ pkgseer pkg info <package> Get package summary
4519
+ pkgseer pkg vulns <package> Check vulnerabilities
4520
+ pkgseer pkg quality <package> Get quality score
4521
+ pkgseer pkg deps <package> List dependencies
4522
+ pkgseer pkg compare <pkg...> Compare packages
4523
+
4524
+ Documentation commands:
4525
+ pkgseer docs list <package> List doc pages
4526
+ pkgseer docs get <pkg> <page> Fetch a doc page
4527
+ pkgseer docs search <query> Search documentation
1057
4528
 
1058
4529
  Learn more at https://pkgseer.dev`);
1059
4530
  registerMcpCommand(program);
1060
4531
  registerLoginCommand(program);
1061
4532
  registerLogoutCommand(program);
4533
+ registerQuickstartCommand(program);
1062
4534
  var auth = program.command("auth").description("View and manage authentication");
1063
4535
  registerAuthStatusCommand(auth);
4536
+ var config = program.command("config").description("View and manage configuration");
4537
+ registerConfigShowCommand(config);
4538
+ var pkg = program.command("pkg").description("Package intelligence commands");
4539
+ registerPkgInfoCommand(pkg);
4540
+ registerPkgQualityCommand(pkg);
4541
+ registerPkgDepsCommand(pkg);
4542
+ registerPkgVulnsCommand(pkg);
4543
+ registerPkgCompareCommand(pkg);
4544
+ var docs = program.command("docs").description("Package documentation commands");
4545
+ registerDocsListCommand(docs);
4546
+ registerDocsGetCommand(docs);
4547
+ registerDocsSearchCommand(docs);
4548
+ var project = program.command("project").description("Project management commands");
4549
+ registerProjectInitCommand(project);
4550
+ registerProjectDetectCommand(project);
4551
+ registerProjectUploadCommand(project);
1064
4552
  program.parse();