@pkgseer/cli 0.1.1 → 0.1.3
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 +2458 -1809
- package/dist/index.js +1 -1
- package/dist/shared/{chunk-3ne7e7xh.js → chunk-8dn0z2ja.js} +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
version
|
|
4
|
-
} from "./shared/chunk-
|
|
4
|
+
} from "./shared/chunk-8dn0z2ja.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -202,7 +202,7 @@ var CliDocsListDocument = gql`
|
|
|
202
202
|
packageName
|
|
203
203
|
version
|
|
204
204
|
pages {
|
|
205
|
-
|
|
205
|
+
id
|
|
206
206
|
title
|
|
207
207
|
words
|
|
208
208
|
}
|
|
@@ -465,6 +465,28 @@ var FetchPackageDocDocument = gql`
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
`;
|
|
468
|
+
var GetDocPageDocument = gql`
|
|
469
|
+
query GetDocPage($pageId: String!) {
|
|
470
|
+
getDocPage(pageId: $pageId) {
|
|
471
|
+
schemaVersion
|
|
472
|
+
page {
|
|
473
|
+
id
|
|
474
|
+
title
|
|
475
|
+
content
|
|
476
|
+
contentFormat
|
|
477
|
+
breadcrumbs
|
|
478
|
+
linkName
|
|
479
|
+
linkTargets
|
|
480
|
+
lastUpdatedAt
|
|
481
|
+
source {
|
|
482
|
+
url
|
|
483
|
+
label
|
|
484
|
+
}
|
|
485
|
+
baseUrl
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
`;
|
|
468
490
|
var SearchPackageDocsDocument = gql`
|
|
469
491
|
query SearchPackageDocs($registry: Registry!, $packageName: String!, $keywords: [String!], $query: String, $includeSnippets: Boolean, $limit: Int, $version: String) {
|
|
470
492
|
searchPackageDocs(
|
|
@@ -548,6 +570,7 @@ var PackageQualityDocumentString = print(PackageQualityDocument);
|
|
|
548
570
|
var ComparePackagesDocumentString = print(ComparePackagesDocument);
|
|
549
571
|
var ListPackageDocsDocumentString = print(ListPackageDocsDocument);
|
|
550
572
|
var FetchPackageDocDocumentString = print(FetchPackageDocDocument);
|
|
573
|
+
var GetDocPageDocumentString = print(GetDocPageDocument);
|
|
551
574
|
var SearchPackageDocsDocumentString = print(SearchPackageDocsDocument);
|
|
552
575
|
var SearchProjectDocsDocumentString = print(SearchProjectDocsDocument);
|
|
553
576
|
function getSdk(client, withWrapper = defaultWrapper) {
|
|
@@ -603,6 +626,9 @@ function getSdk(client, withWrapper = defaultWrapper) {
|
|
|
603
626
|
FetchPackageDoc(variables, requestHeaders) {
|
|
604
627
|
return withWrapper((wrappedRequestHeaders) => client.rawRequest(FetchPackageDocDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "FetchPackageDoc", "query", variables);
|
|
605
628
|
},
|
|
629
|
+
GetDocPage(variables, requestHeaders) {
|
|
630
|
+
return withWrapper((wrappedRequestHeaders) => client.rawRequest(GetDocPageDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "GetDocPage", "query", variables);
|
|
631
|
+
},
|
|
606
632
|
SearchPackageDocs(variables, requestHeaders) {
|
|
607
633
|
return withWrapper((wrappedRequestHeaders) => client.rawRequest(SearchPackageDocsDocumentString, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "SearchPackageDocs", "query", variables);
|
|
608
634
|
},
|
|
@@ -824,7 +850,7 @@ class AuthStorageImpl {
|
|
|
824
850
|
const normalizedUrl = normalizeBaseUrl(baseUrl);
|
|
825
851
|
stored.tokens[normalizedUrl] = token;
|
|
826
852
|
stored.version = CURRENT_VERSION;
|
|
827
|
-
await this.fs.ensureDir(this.
|
|
853
|
+
await this.fs.ensureDir(this.configDir, DIR_MODE);
|
|
828
854
|
await this.fs.writeFile(this.authPath, JSON.stringify(stored, null, 2), FILE_MODE);
|
|
829
855
|
}
|
|
830
856
|
async clear(baseUrl) {
|
|
@@ -900,29 +926,18 @@ function migrateV2ToV3(legacy) {
|
|
|
900
926
|
}
|
|
901
927
|
// src/services/auth-utils.ts
|
|
902
928
|
var PROJECT_MANIFEST_UPLOAD_SCOPE = "project_manifest_upload";
|
|
903
|
-
async function checkProjectWriteScope(
|
|
904
|
-
const
|
|
905
|
-
if (
|
|
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) {
|
|
929
|
+
async function checkProjectWriteScope(configService, baseUrl) {
|
|
930
|
+
const tokenData = await configService.getApiToken(baseUrl);
|
|
931
|
+
if (!tokenData) {
|
|
917
932
|
return null;
|
|
918
933
|
}
|
|
919
|
-
if (
|
|
920
|
-
return
|
|
934
|
+
if (tokenData.scopes.length === 0) {
|
|
935
|
+
return tokenData;
|
|
921
936
|
}
|
|
922
|
-
if (!
|
|
937
|
+
if (!tokenData.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
|
|
923
938
|
return null;
|
|
924
939
|
}
|
|
925
|
-
return
|
|
940
|
+
return tokenData;
|
|
926
941
|
}
|
|
927
942
|
// src/services/browser-service.ts
|
|
928
943
|
import open from "open";
|
|
@@ -971,11 +986,14 @@ var MergedConfigSchema = z.object({
|
|
|
971
986
|
project: z.string().optional(),
|
|
972
987
|
manifests: z.array(ManifestGroupSchema).optional()
|
|
973
988
|
});
|
|
989
|
+
var PKGSEER_API_TOKEN = "PKGSEER_API_TOKEN";
|
|
974
990
|
|
|
975
991
|
class ConfigServiceImpl {
|
|
976
992
|
fs;
|
|
977
|
-
|
|
993
|
+
authStorage;
|
|
994
|
+
constructor(fs, authStorage) {
|
|
978
995
|
this.fs = fs;
|
|
996
|
+
this.authStorage = authStorage;
|
|
979
997
|
}
|
|
980
998
|
getGlobalConfigPath() {
|
|
981
999
|
return this.fs.joinPath(this.fs.getHomeDir(), CONFIG_DIR2, GLOBAL_CONFIG_FILE);
|
|
@@ -1059,6 +1077,27 @@ class ConfigServiceImpl {
|
|
|
1059
1077
|
return null;
|
|
1060
1078
|
}
|
|
1061
1079
|
}
|
|
1080
|
+
async getApiToken(baseUrl) {
|
|
1081
|
+
const envToken = process.env[PKGSEER_API_TOKEN];
|
|
1082
|
+
if (envToken) {
|
|
1083
|
+
return {
|
|
1084
|
+
token: envToken,
|
|
1085
|
+
tokenName: "PKGSEER_API_TOKEN",
|
|
1086
|
+
scopes: [],
|
|
1087
|
+
createdAt: new Date().toISOString(),
|
|
1088
|
+
expiresAt: null,
|
|
1089
|
+
apiKeyId: 0
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const stored = await this.authStorage.load(baseUrl);
|
|
1093
|
+
if (!stored) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (stored.expiresAt && new Date(stored.expiresAt) < new Date) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
return stored;
|
|
1100
|
+
}
|
|
1062
1101
|
}
|
|
1063
1102
|
// src/services/filesystem-service.ts
|
|
1064
1103
|
import {
|
|
@@ -1097,7 +1136,7 @@ class FileSystemServiceImpl {
|
|
|
1097
1136
|
}
|
|
1098
1137
|
}
|
|
1099
1138
|
async ensureDir(path, mode) {
|
|
1100
|
-
await mkdir(
|
|
1139
|
+
await mkdir(path, { recursive: true, mode });
|
|
1101
1140
|
}
|
|
1102
1141
|
getHomeDir() {
|
|
1103
1142
|
return homedir();
|
|
@@ -1207,6 +1246,10 @@ class PkgseerServiceImpl {
|
|
|
1207
1246
|
});
|
|
1208
1247
|
return { data: result.data, errors: result.errors };
|
|
1209
1248
|
}
|
|
1249
|
+
async getDocPage(pageId) {
|
|
1250
|
+
const result = await this.client.GetDocPage({ pageId });
|
|
1251
|
+
return { data: result.data, errors: result.errors };
|
|
1252
|
+
}
|
|
1210
1253
|
async searchPackageDocs(registry, packageName, options) {
|
|
1211
1254
|
const result = await this.client.SearchPackageDocs({
|
|
1212
1255
|
registry,
|
|
@@ -1523,27 +1566,17 @@ class ShellServiceImpl {
|
|
|
1523
1566
|
}
|
|
1524
1567
|
}
|
|
1525
1568
|
// src/container.ts
|
|
1526
|
-
async function resolveApiToken(
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
return envToken;
|
|
1530
|
-
}
|
|
1531
|
-
const stored = await authStorage.load(baseUrl);
|
|
1532
|
-
if (stored) {
|
|
1533
|
-
if (stored.expiresAt && new Date(stored.expiresAt) < new Date) {
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
return stored.token;
|
|
1537
|
-
}
|
|
1538
|
-
return;
|
|
1569
|
+
async function resolveApiToken(configService, baseUrl) {
|
|
1570
|
+
const tokenData = await configService.getApiToken(baseUrl);
|
|
1571
|
+
return tokenData?.token;
|
|
1539
1572
|
}
|
|
1540
1573
|
async function createContainer() {
|
|
1541
1574
|
const baseUrl = getBaseUrl();
|
|
1542
1575
|
const fileSystemService = new FileSystemServiceImpl;
|
|
1543
1576
|
const authStorage = new AuthStorageImpl(fileSystemService);
|
|
1544
|
-
const configService = new ConfigServiceImpl(fileSystemService);
|
|
1577
|
+
const configService = new ConfigServiceImpl(fileSystemService, authStorage);
|
|
1545
1578
|
const [apiToken, configResult] = await Promise.all([
|
|
1546
|
-
resolveApiToken(
|
|
1579
|
+
resolveApiToken(configService, baseUrl),
|
|
1547
1580
|
configService.loadMergedConfig()
|
|
1548
1581
|
]);
|
|
1549
1582
|
const client = createClient(apiToken);
|
|
@@ -1789,10 +1822,11 @@ function formatScore(score) {
|
|
|
1789
1822
|
// src/commands/docs/get.ts
|
|
1790
1823
|
function parsePackageRef(ref) {
|
|
1791
1824
|
if (!ref.includes("/")) {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1825
|
+
return {
|
|
1826
|
+
pageId: ref,
|
|
1827
|
+
originalRef: ref,
|
|
1828
|
+
isGlobalId: true
|
|
1829
|
+
};
|
|
1796
1830
|
}
|
|
1797
1831
|
const parts = ref.split("/");
|
|
1798
1832
|
if (parts.length < 2) {
|
|
@@ -1821,7 +1855,7 @@ function parsePackageRef(ref) {
|
|
|
1821
1855
|
}
|
|
1822
1856
|
return { packageName, pageId, originalRef: ref };
|
|
1823
1857
|
}
|
|
1824
|
-
function formatDocPage(data, ref) {
|
|
1858
|
+
function formatDocPage(data, ref, verbose = false) {
|
|
1825
1859
|
const lines = [];
|
|
1826
1860
|
const page = data.page;
|
|
1827
1861
|
if (!page) {
|
|
@@ -1829,6 +1863,49 @@ function formatDocPage(data, ref) {
|
|
|
1829
1863
|
}
|
|
1830
1864
|
lines.push(`[${ref}]`);
|
|
1831
1865
|
lines.push("");
|
|
1866
|
+
if (verbose) {
|
|
1867
|
+
const metadata = [];
|
|
1868
|
+
if (data.schemaVersion) {
|
|
1869
|
+
metadata.push(`Schema Version: ${data.schemaVersion}`);
|
|
1870
|
+
}
|
|
1871
|
+
if (page.id) {
|
|
1872
|
+
metadata.push(`Page ID: ${page.id}`);
|
|
1873
|
+
}
|
|
1874
|
+
if (page.contentFormat) {
|
|
1875
|
+
metadata.push(`Format: ${page.contentFormat}`);
|
|
1876
|
+
}
|
|
1877
|
+
if (page.lastUpdatedAt) {
|
|
1878
|
+
metadata.push(`Last Updated: ${page.lastUpdatedAt}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (page.source) {
|
|
1881
|
+
const sourceParts = [];
|
|
1882
|
+
if (page.source.url) {
|
|
1883
|
+
sourceParts.push(page.source.url);
|
|
1884
|
+
}
|
|
1885
|
+
if (page.source.label) {
|
|
1886
|
+
sourceParts.push(`(${page.source.label})`);
|
|
1887
|
+
}
|
|
1888
|
+
if (sourceParts.length > 0) {
|
|
1889
|
+
metadata.push(`Source: ${sourceParts.join(" ")}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (page.baseUrl) {
|
|
1893
|
+
metadata.push(`Base URL: ${page.baseUrl}`);
|
|
1894
|
+
}
|
|
1895
|
+
if (page.linkName) {
|
|
1896
|
+
metadata.push(`Link Name: ${page.linkName}`);
|
|
1897
|
+
}
|
|
1898
|
+
if (page.linkTargets && page.linkTargets.length > 0) {
|
|
1899
|
+
metadata.push(`Link Targets: ${page.linkTargets.join(", ")}`);
|
|
1900
|
+
}
|
|
1901
|
+
if (metadata.length > 0) {
|
|
1902
|
+
lines.push("Metadata:");
|
|
1903
|
+
for (const meta of metadata) {
|
|
1904
|
+
lines.push(` ${meta}`);
|
|
1905
|
+
}
|
|
1906
|
+
lines.push("");
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1832
1909
|
if (page.breadcrumbs && page.breadcrumbs.length > 0) {
|
|
1833
1910
|
lines.push(page.breadcrumbs.join(" > "));
|
|
1834
1911
|
lines.push("");
|
|
@@ -1858,13 +1935,66 @@ function extractErrorMessage(error) {
|
|
|
1858
1935
|
}
|
|
1859
1936
|
async function fetchPage(ref, defaultRegistry, defaultVersion, pkgseerService) {
|
|
1860
1937
|
try {
|
|
1938
|
+
if (ref.isGlobalId) {
|
|
1939
|
+
const result2 = await pkgseerService.getDocPage(ref.pageId);
|
|
1940
|
+
if (result2.errors && result2.errors.length > 0) {
|
|
1941
|
+
const errorMsg = result2.errors.map((e) => {
|
|
1942
|
+
if (typeof e === "object" && e !== null) {
|
|
1943
|
+
const parts = [];
|
|
1944
|
+
if ("message" in e) {
|
|
1945
|
+
parts.push(String(e.message));
|
|
1946
|
+
}
|
|
1947
|
+
if ("extensions" in e && e.extensions) {
|
|
1948
|
+
const ext = e.extensions;
|
|
1949
|
+
if (typeof ext === "object" && "code" in ext) {
|
|
1950
|
+
parts.push(`Code: ${ext.code}`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return parts.length > 0 ? parts.join(" ") : JSON.stringify(e);
|
|
1954
|
+
}
|
|
1955
|
+
return String(e);
|
|
1956
|
+
}).join("; ");
|
|
1957
|
+
return { ref: ref.originalRef, result: null, error: errorMsg };
|
|
1958
|
+
}
|
|
1959
|
+
if (!result2.data.getDocPage) {
|
|
1960
|
+
return {
|
|
1961
|
+
ref: ref.originalRef,
|
|
1962
|
+
result: null,
|
|
1963
|
+
error: `Page not found: ${ref.pageId}`
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
return {
|
|
1967
|
+
ref: ref.originalRef,
|
|
1968
|
+
result: {
|
|
1969
|
+
schemaVersion: result2.data.getDocPage?.schemaVersion ?? null,
|
|
1970
|
+
page: result2.data.getDocPage?.page ?? null
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1861
1974
|
const registry = ref.registry ? toGraphQLRegistry(ref.registry) : defaultRegistry;
|
|
1862
1975
|
const version2 = ref.version ?? defaultVersion;
|
|
1976
|
+
if (!ref.packageName) {
|
|
1977
|
+
return {
|
|
1978
|
+
ref: ref.originalRef,
|
|
1979
|
+
result: null,
|
|
1980
|
+
error: `Package name required for reference: ${ref.originalRef}`
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1863
1983
|
const result = await pkgseerService.cliDocsGet(registry, ref.packageName, ref.pageId, version2);
|
|
1864
1984
|
if (result.errors && result.errors.length > 0) {
|
|
1865
1985
|
const errorMsg = result.errors.map((e) => {
|
|
1866
|
-
if (typeof e === "object" && e !== null
|
|
1867
|
-
|
|
1986
|
+
if (typeof e === "object" && e !== null) {
|
|
1987
|
+
const parts = [];
|
|
1988
|
+
if ("message" in e) {
|
|
1989
|
+
parts.push(String(e.message));
|
|
1990
|
+
}
|
|
1991
|
+
if ("extensions" in e && e.extensions) {
|
|
1992
|
+
const ext = e.extensions;
|
|
1993
|
+
if (typeof ext === "object" && "code" in ext) {
|
|
1994
|
+
parts.push(`Code: ${ext.code}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return parts.length > 0 ? parts.join(" ") : JSON.stringify(e);
|
|
1868
1998
|
}
|
|
1869
1999
|
return String(e);
|
|
1870
2000
|
}).join("; ");
|
|
@@ -1877,7 +2007,12 @@ async function fetchPage(ref, defaultRegistry, defaultVersion, pkgseerService) {
|
|
|
1877
2007
|
error: `Page not found: ${ref.pageId} for ${ref.packageName}`
|
|
1878
2008
|
};
|
|
1879
2009
|
}
|
|
1880
|
-
return {
|
|
2010
|
+
return {
|
|
2011
|
+
ref: ref.originalRef,
|
|
2012
|
+
result: result.data.fetchPackageDoc ? {
|
|
2013
|
+
page: result.data.fetchPackageDoc.page ?? null
|
|
2014
|
+
} : null
|
|
2015
|
+
};
|
|
1881
2016
|
} catch (error) {
|
|
1882
2017
|
return {
|
|
1883
2018
|
ref: ref.originalRef,
|
|
@@ -1888,7 +2023,7 @@ async function fetchPage(ref, defaultRegistry, defaultVersion, pkgseerService) {
|
|
|
1888
2023
|
}
|
|
1889
2024
|
async function docsGetAction(refs, options, deps) {
|
|
1890
2025
|
if (refs.length === 0) {
|
|
1891
|
-
outputError("At least one page reference required. Format: <package>/<
|
|
2026
|
+
outputError("At least one page reference required. Format: <globally-unique-id> or <package>/<document>", options.json ?? false);
|
|
1892
2027
|
return;
|
|
1893
2028
|
}
|
|
1894
2029
|
const { pkgseerService } = deps;
|
|
@@ -1926,17 +2061,24 @@ ${errorMessages}`, options.json ?? false);
|
|
|
1926
2061
|
if (r.error) {
|
|
1927
2062
|
return { ref: r.ref, error: r.error };
|
|
1928
2063
|
}
|
|
2064
|
+
if (options.verbose) {
|
|
2065
|
+
return {
|
|
2066
|
+
ref: r.ref,
|
|
2067
|
+
...r.result
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
1929
2070
|
const page = r.result?.page;
|
|
1930
2071
|
return {
|
|
1931
2072
|
ref: r.ref,
|
|
1932
2073
|
title: page?.title,
|
|
1933
|
-
content: page?.content
|
|
2074
|
+
content: page?.content,
|
|
2075
|
+
breadcrumbs: page?.breadcrumbs
|
|
1934
2076
|
};
|
|
1935
2077
|
});
|
|
1936
2078
|
output(jsonResults, true);
|
|
1937
2079
|
} else {
|
|
1938
2080
|
if (successes.length > 0) {
|
|
1939
|
-
const pages = successes.filter((r) => r.result !== null).map((r) => formatDocPage(r.result, r.ref));
|
|
2081
|
+
const pages = successes.filter((r) => r.result !== null).map((r) => formatDocPage(r.result, r.ref, options.verbose));
|
|
1940
2082
|
console.log(pages.join(`
|
|
1941
2083
|
|
|
1942
2084
|
---
|
|
@@ -1953,29 +2095,28 @@ var GET_DESCRIPTION = `Fetch one or more documentation pages.
|
|
|
1953
2095
|
Retrieves the full content of documentation pages including
|
|
1954
2096
|
title, breadcrumbs, content (markdown), and source URL.
|
|
1955
2097
|
|
|
1956
|
-
Use 'pkgseer docs list' first to discover available page IDs
|
|
1957
|
-
use the output format from 'pkgseer docs search --refs-only'.
|
|
2098
|
+
Use 'pkgseer docs list' first to discover available page IDs.
|
|
1958
2099
|
|
|
1959
|
-
Format: <
|
|
2100
|
+
Format: <globally-unique-id> (preferred)
|
|
2101
|
+
or <registry>/<package>/<version>/<document> (full form)
|
|
1960
2102
|
or <package>/<document> (short form, requires --registry/--pkg-version)
|
|
1961
2103
|
|
|
1962
2104
|
Examples:
|
|
1963
|
-
#
|
|
2105
|
+
# Globally unique ID (from list output)
|
|
2106
|
+
pkgseer docs get 24293-shared-plugins-during-build-2
|
|
2107
|
+
|
|
2108
|
+
# Full form
|
|
1964
2109
|
pkgseer docs get npm/express/4.18.2/readme
|
|
1965
2110
|
pkgseer docs get hex/postgrex/1.15.0/readme
|
|
1966
2111
|
|
|
1967
|
-
# Short form
|
|
2112
|
+
# Short form
|
|
1968
2113
|
pkgseer docs get express/readme --registry npm --pkg-version 4.18.2
|
|
1969
2114
|
pkgseer docs get postgrex/readme --registry hex
|
|
1970
2115
|
|
|
1971
2116
|
# Multiple pages
|
|
1972
|
-
pkgseer docs get
|
|
1973
|
-
|
|
1974
|
-
# Pipe from search (full form works seamlessly)
|
|
1975
|
-
pkgseer docs search log --package express --refs-only | \\
|
|
1976
|
-
xargs pkgseer docs get`;
|
|
2117
|
+
pkgseer docs get 24293-shared-plugins-during-build-2 npm/express/4.18.2/readme`;
|
|
1977
2118
|
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) => {
|
|
2119
|
+
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").option("--verbose", "Include metadata (ID, format, source, links, etc.)").action(async (refs, options) => {
|
|
1979
2120
|
await withCliErrorHandling(options.json ?? false, async () => {
|
|
1980
2121
|
const deps = await createContainer();
|
|
1981
2122
|
await docsGetAction(refs, options, deps);
|
|
@@ -1995,11 +2136,11 @@ function formatDocsList(docs) {
|
|
|
1995
2136
|
lines.push(`Found ${docs.pages.length} pages:`);
|
|
1996
2137
|
lines.push("");
|
|
1997
2138
|
for (const page of docs.pages) {
|
|
1998
|
-
if (!page)
|
|
2139
|
+
if (!page || !page.id)
|
|
1999
2140
|
continue;
|
|
2000
2141
|
const words = page.words ? ` (${formatNumber(page.words)} words)` : "";
|
|
2001
2142
|
lines.push(` ${page.title}${words}`);
|
|
2002
|
-
lines.push(` ID: ${page.
|
|
2143
|
+
lines.push(` ID: ${page.id}`);
|
|
2003
2144
|
lines.push("");
|
|
2004
2145
|
}
|
|
2005
2146
|
lines.push("Use 'pkgseer docs get <package>/<page-id>' to fetch a page.");
|
|
@@ -2017,10 +2158,14 @@ async function docsListAction(packageName, options, deps) {
|
|
|
2017
2158
|
}
|
|
2018
2159
|
if (options.json) {
|
|
2019
2160
|
const pages = result.data.listPackageDocs.pages?.filter((p) => p) ?? [];
|
|
2020
|
-
const slim = pages.map((p) =>
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2161
|
+
const slim = pages.map((p) => {
|
|
2162
|
+
if (!p || !p.id)
|
|
2163
|
+
return null;
|
|
2164
|
+
return {
|
|
2165
|
+
id: p.id,
|
|
2166
|
+
title: p.title
|
|
2167
|
+
};
|
|
2168
|
+
}).filter((p) => p !== null);
|
|
2024
2169
|
output(slim, true);
|
|
2025
2170
|
} else {
|
|
2026
2171
|
console.log(formatDocsList(result.data.listPackageDocs));
|
|
@@ -2028,7 +2173,7 @@ async function docsListAction(packageName, options, deps) {
|
|
|
2028
2173
|
}
|
|
2029
2174
|
var LIST_DESCRIPTION = `List available documentation pages for a package.
|
|
2030
2175
|
|
|
2031
|
-
Shows all documentation pages with titles, page IDs
|
|
2176
|
+
Shows all documentation pages with titles, globally unique page IDs,
|
|
2032
2177
|
word counts, and descriptions. Use the page ID with 'docs get'
|
|
2033
2178
|
to fetch the full content of a specific page.
|
|
2034
2179
|
|
|
@@ -2305,1906 +2450,2422 @@ function registerDocsSearchCommand(program) {
|
|
|
2305
2450
|
});
|
|
2306
2451
|
});
|
|
2307
2452
|
}
|
|
2308
|
-
// src/commands/
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2453
|
+
// src/commands/shared-colors.ts
|
|
2454
|
+
var colors2 = {
|
|
2455
|
+
reset: "\x1B[0m",
|
|
2456
|
+
bold: "\x1B[1m",
|
|
2457
|
+
dim: "\x1B[2m",
|
|
2458
|
+
green: "\x1B[32m",
|
|
2459
|
+
yellow: "\x1B[33m",
|
|
2460
|
+
blue: "\x1B[34m",
|
|
2461
|
+
magenta: "\x1B[35m",
|
|
2462
|
+
cyan: "\x1B[36m",
|
|
2463
|
+
red: "\x1B[31m"
|
|
2464
|
+
};
|
|
2465
|
+
function shouldUseColors2(noColor) {
|
|
2466
|
+
if (noColor)
|
|
2467
|
+
return false;
|
|
2468
|
+
if (process.env.NO_COLOR !== undefined)
|
|
2469
|
+
return false;
|
|
2470
|
+
return process.stdout.isTTY ?? false;
|
|
2313
2471
|
}
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2472
|
+
function success(text, useColors) {
|
|
2473
|
+
const checkmark = useColors ? `${colors2.green}✓${colors2.reset}` : "✓";
|
|
2474
|
+
return `${checkmark} ${text}`;
|
|
2475
|
+
}
|
|
2476
|
+
function error(text, useColors) {
|
|
2477
|
+
const cross = useColors ? `${colors2.red}✗${colors2.reset}` : "✗";
|
|
2478
|
+
return `${cross} ${text}`;
|
|
2479
|
+
}
|
|
2480
|
+
function highlight(text, useColors) {
|
|
2481
|
+
if (!useColors)
|
|
2482
|
+
return text;
|
|
2483
|
+
return `${colors2.bold}${colors2.cyan}${text}${colors2.reset}`;
|
|
2484
|
+
}
|
|
2485
|
+
function dim(text, useColors) {
|
|
2486
|
+
if (!useColors)
|
|
2487
|
+
return text;
|
|
2488
|
+
return `${colors2.dim}${text}${colors2.reset}`;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// src/commands/mcp-init.ts
|
|
2492
|
+
function getCursorConfigPaths(fs, scope) {
|
|
2493
|
+
if (scope === "project") {
|
|
2494
|
+
const cwd = fs.getCwd();
|
|
2495
|
+
const configPath2 = fs.joinPath(cwd, ".cursor", "mcp.json");
|
|
2496
|
+
const backupPath2 = fs.joinPath(cwd, ".cursor", "mcp.json.bak");
|
|
2497
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2498
|
+
}
|
|
2499
|
+
const platform = process.platform;
|
|
2500
|
+
let configPath;
|
|
2501
|
+
let backupPath;
|
|
2502
|
+
if (platform === "win32") {
|
|
2503
|
+
const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
|
|
2504
|
+
configPath = fs.joinPath(appData, "Cursor", "mcp.json");
|
|
2505
|
+
backupPath = fs.joinPath(appData, "Cursor", "mcp.json.bak");
|
|
2506
|
+
} else {
|
|
2507
|
+
const home = fs.getHomeDir();
|
|
2508
|
+
configPath = fs.joinPath(home, ".cursor", "mcp.json");
|
|
2509
|
+
backupPath = fs.joinPath(home, ".cursor", "mcp.json.bak");
|
|
2510
|
+
}
|
|
2511
|
+
return { configPath, backupPath };
|
|
2512
|
+
}
|
|
2513
|
+
function getCodexConfigPaths(fs, scope) {
|
|
2514
|
+
if (scope === "project") {
|
|
2515
|
+
const cwd = fs.getCwd();
|
|
2516
|
+
const configPath2 = fs.joinPath(cwd, ".codex", "config.toml");
|
|
2517
|
+
const backupPath2 = fs.joinPath(cwd, ".codex", "config.toml.bak");
|
|
2518
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2519
|
+
}
|
|
2520
|
+
const home = fs.getHomeDir();
|
|
2521
|
+
const configPath = fs.joinPath(home, ".codex", "config.toml");
|
|
2522
|
+
const backupPath = fs.joinPath(home, ".codex", "config.toml.bak");
|
|
2523
|
+
return { configPath, backupPath };
|
|
2524
|
+
}
|
|
2525
|
+
function getClaudeCodeConfigPaths(fs, scope) {
|
|
2526
|
+
if (scope === "project") {
|
|
2527
|
+
const cwd = fs.getCwd();
|
|
2528
|
+
const configPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json");
|
|
2529
|
+
const backupPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json.bak");
|
|
2530
|
+
return { configPath: configPath2, backupPath: backupPath2 };
|
|
2531
|
+
}
|
|
2532
|
+
const platform = process.platform;
|
|
2533
|
+
let configPath;
|
|
2534
|
+
let backupPath;
|
|
2535
|
+
if (platform === "win32") {
|
|
2536
|
+
const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
|
|
2537
|
+
configPath = fs.joinPath(appData, "Claude Code", "mcp.json");
|
|
2538
|
+
backupPath = fs.joinPath(appData, "Claude Code", "mcp.json.bak");
|
|
2539
|
+
} else {
|
|
2540
|
+
const home = fs.getHomeDir();
|
|
2541
|
+
configPath = fs.joinPath(home, ".claude-code", "mcp.json");
|
|
2542
|
+
backupPath = fs.joinPath(home, ".claude-code", "mcp.json.bak");
|
|
2334
2543
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2544
|
+
return { configPath, backupPath };
|
|
2545
|
+
}
|
|
2546
|
+
async function parseConfigFile(fs, path) {
|
|
2547
|
+
const exists = await fs.exists(path);
|
|
2548
|
+
if (!exists) {
|
|
2549
|
+
return null;
|
|
2550
|
+
}
|
|
2551
|
+
try {
|
|
2552
|
+
const content = await fs.readFile(path);
|
|
2553
|
+
const parsed = JSON.parse(content);
|
|
2554
|
+
return parsed;
|
|
2555
|
+
} catch {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
async function writeConfigFile(fs, path, config) {
|
|
2560
|
+
const content = JSON.stringify(config, null, 2);
|
|
2561
|
+
await fs.writeFile(path, content);
|
|
2562
|
+
}
|
|
2563
|
+
async function backupConfigFile(fs, configPath, backupPath) {
|
|
2564
|
+
const exists = await fs.exists(configPath);
|
|
2565
|
+
if (!exists) {
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
const content = await fs.readFile(configPath);
|
|
2569
|
+
await fs.writeFile(backupPath, content);
|
|
2570
|
+
}
|
|
2571
|
+
async function canSafelyEdit(fs, configPath) {
|
|
2572
|
+
const exists = await fs.exists(configPath);
|
|
2573
|
+
if (!exists) {
|
|
2574
|
+
return { safe: true };
|
|
2575
|
+
}
|
|
2576
|
+
const config = await parseConfigFile(fs, configPath);
|
|
2577
|
+
if (config === null) {
|
|
2578
|
+
return {
|
|
2579
|
+
safe: false,
|
|
2580
|
+
reason: "Config file exists but cannot be parsed as valid JSON"
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
if (config.mcpServers?.pkgseer) {
|
|
2584
|
+
return {
|
|
2585
|
+
safe: false,
|
|
2586
|
+
reason: "PkgSeer MCP server is already configured"
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
return { safe: true };
|
|
2590
|
+
}
|
|
2591
|
+
function addPkgseerToConfig(config) {
|
|
2592
|
+
const updated = { ...config };
|
|
2593
|
+
if (!updated.mcpServers) {
|
|
2594
|
+
updated.mcpServers = {};
|
|
2595
|
+
}
|
|
2596
|
+
updated.mcpServers.pkgseer = {
|
|
2597
|
+
command: "npx",
|
|
2598
|
+
args: ["-y", "@pkgseer/cli", "mcp", "start"]
|
|
2599
|
+
};
|
|
2600
|
+
return updated;
|
|
2601
|
+
}
|
|
2602
|
+
function showManualInstructions(tool, scope, configPath, useColors) {
|
|
2603
|
+
console.log(`
|
|
2604
|
+
Manual Setup Instructions`);
|
|
2605
|
+
console.log(`─────────────────────────
|
|
2346
2606
|
`);
|
|
2347
|
-
|
|
2607
|
+
console.log(`Config file: ${highlight(configPath, useColors)}
|
|
2608
|
+
`);
|
|
2609
|
+
console.log(`Add the following to your configuration:
|
|
2348
2610
|
`);
|
|
2611
|
+
if (tool === "codex") {
|
|
2612
|
+
const tomlConfig = `[mcp_servers.pkgseer]
|
|
2613
|
+
command = "npx"
|
|
2614
|
+
args = ["-y", "@pkgseer/cli", "mcp", "start"]`;
|
|
2615
|
+
console.log(tomlConfig);
|
|
2349
2616
|
} else {
|
|
2350
|
-
|
|
2351
|
-
|
|
2617
|
+
const configExample = {
|
|
2618
|
+
mcpServers: {
|
|
2619
|
+
pkgseer: {
|
|
2620
|
+
command: "npx",
|
|
2621
|
+
args: ["-y", "@pkgseer/cli", "mcp", "start"]
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
console.log(JSON.stringify(configExample, null, 2));
|
|
2352
2626
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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);
|
|
2627
|
+
if ((tool === "cursor" || tool === "codex" || tool === "claude-code") && scope === "project") {
|
|
2628
|
+
const dirName = tool === "cursor" ? ".cursor" : tool === "codex" ? ".codex" : ".claude-code";
|
|
2629
|
+
console.log(dim(`
|
|
2630
|
+
Note: Create the ${dirName} directory if it doesn't exist.`, useColors));
|
|
2371
2631
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2632
|
+
console.log(dim(`
|
|
2633
|
+
After editing, restart your AI assistant to activate the MCP server.`, useColors));
|
|
2634
|
+
}
|
|
2635
|
+
async function mcpInitAction(deps) {
|
|
2636
|
+
const { fileSystemService: fs, promptService, hasProject } = deps;
|
|
2637
|
+
const useColors = shouldUseColors2();
|
|
2638
|
+
console.log("MCP Server Setup");
|
|
2639
|
+
console.log(`────────────────
|
|
2374
2640
|
`);
|
|
2375
|
-
|
|
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}
|
|
2641
|
+
console.log(`Configure PkgSeer as an MCP server for your AI assistant.
|
|
2387
2642
|
`);
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
apiKeyId: tokenResponse.apiKeyId
|
|
2398
|
-
});
|
|
2399
|
-
console.log(`✓ Logged in
|
|
2643
|
+
if (!hasProject) {
|
|
2644
|
+
console.log(dim(`Note: No pkgseer.yml found in this directory.
|
|
2645
|
+
`, useColors));
|
|
2646
|
+
console.log(dim(`Without it, search_project_docs won't work (searches your project's packages).
|
|
2647
|
+
`, useColors));
|
|
2648
|
+
const setupProject = await promptService.confirm("Set up project configuration first?", false);
|
|
2649
|
+
if (setupProject) {
|
|
2650
|
+
console.log(`
|
|
2651
|
+
Run ${highlight("pkgseer project init", useColors)} first, then ${highlight("pkgseer mcp init", useColors)} again.
|
|
2400
2652
|
`);
|
|
2401
|
-
|
|
2402
|
-
|
|
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`);
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2406
2655
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2656
|
+
const tool = await promptService.select("Which AI tool would you like to configure?", [
|
|
2657
|
+
{
|
|
2658
|
+
value: "cursor",
|
|
2659
|
+
name: "Cursor IDE",
|
|
2660
|
+
description: "Project-level or global configuration"
|
|
2661
|
+
},
|
|
2662
|
+
{
|
|
2663
|
+
value: "codex",
|
|
2664
|
+
name: "Codex CLI",
|
|
2665
|
+
description: "Project-level or global configuration"
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
value: "claude-code",
|
|
2669
|
+
name: "Claude Code",
|
|
2670
|
+
description: "Project-level or global configuration"
|
|
2671
|
+
},
|
|
2672
|
+
{
|
|
2673
|
+
value: "other",
|
|
2674
|
+
name: "Other",
|
|
2675
|
+
description: "Show manual setup instructions"
|
|
2676
|
+
}
|
|
2677
|
+
]);
|
|
2678
|
+
if (tool === "other") {
|
|
2679
|
+
const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
|
|
2680
|
+
showManualInstructions("other", undefined, configPath2, useColors);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
let scope;
|
|
2684
|
+
if (tool === "cursor" || tool === "codex" || tool === "claude-code") {
|
|
2685
|
+
const projectPath = tool === "cursor" ? ".cursor/mcp.json" : tool === "codex" ? ".codex/config.toml" : ".claude-code/mcp.json";
|
|
2686
|
+
const globalPath = tool === "cursor" ? "~/.cursor/mcp.json" : tool === "codex" ? "~/.codex/config.toml" : "~/.claude-code/mcp.json";
|
|
2687
|
+
if (hasProject) {
|
|
2688
|
+
scope = await promptService.select("Where should the MCP config be created?", [
|
|
2689
|
+
{
|
|
2690
|
+
value: "project",
|
|
2691
|
+
name: `Project (${projectPath}) – recommended`,
|
|
2692
|
+
description: "Uses project token; enables docs search for your packages"
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
value: "global",
|
|
2696
|
+
name: `Global (${globalPath})`,
|
|
2697
|
+
description: "Works everywhere but without project-specific features"
|
|
2698
|
+
}
|
|
2699
|
+
]);
|
|
2700
|
+
} else {
|
|
2701
|
+
console.log(dim(`
|
|
2702
|
+
Using global config (no pkgseer.yml found).
|
|
2703
|
+
`, useColors));
|
|
2704
|
+
scope = "global";
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
let configPath;
|
|
2708
|
+
let backupPath;
|
|
2709
|
+
if (tool === "cursor") {
|
|
2710
|
+
if (!scope) {
|
|
2711
|
+
scope = "global";
|
|
2712
|
+
}
|
|
2713
|
+
const paths = getCursorConfigPaths(fs, scope);
|
|
2714
|
+
configPath = paths.configPath;
|
|
2715
|
+
backupPath = paths.backupPath;
|
|
2716
|
+
} else if (tool === "codex") {
|
|
2717
|
+
if (!scope) {
|
|
2718
|
+
scope = hasProject ? "project" : "global";
|
|
2719
|
+
}
|
|
2720
|
+
const paths = getCodexConfigPaths(fs, scope);
|
|
2721
|
+
showManualInstructions("codex", scope, paths.configPath, useColors);
|
|
2722
|
+
return;
|
|
2723
|
+
} else if (tool === "claude-code") {
|
|
2724
|
+
if (!scope) {
|
|
2725
|
+
scope = "global";
|
|
2726
|
+
}
|
|
2727
|
+
const paths = getClaudeCodeConfigPaths(fs, scope);
|
|
2728
|
+
configPath = paths.configPath;
|
|
2729
|
+
backupPath = paths.backupPath;
|
|
2730
|
+
} else {
|
|
2731
|
+
const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
|
|
2732
|
+
showManualInstructions("other", undefined, configPath2, useColors);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const safetyCheck = await canSafelyEdit(fs, configPath);
|
|
2736
|
+
if (!safetyCheck.safe) {
|
|
2737
|
+
console.log(error(`Cannot safely edit config file: ${safetyCheck.reason}`, useColors));
|
|
2738
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2432
2739
|
return;
|
|
2433
2740
|
}
|
|
2741
|
+
const configExists = await fs.exists(configPath);
|
|
2742
|
+
if (configExists) {
|
|
2743
|
+
console.log(`
|
|
2744
|
+
Found existing config at: ${highlight(configPath, useColors)}`);
|
|
2745
|
+
const proceed = await promptService.confirm("Add PkgSeer to this configuration?", true);
|
|
2746
|
+
if (!proceed) {
|
|
2747
|
+
console.log(dim(`
|
|
2748
|
+
Setup cancelled.`, useColors));
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
} else {
|
|
2752
|
+
console.log(`
|
|
2753
|
+
Will create config at: ${highlight(configPath, useColors)}`);
|
|
2754
|
+
const proceed = await promptService.confirm("Proceed?", true);
|
|
2755
|
+
if (!proceed) {
|
|
2756
|
+
console.log(dim(`
|
|
2757
|
+
Setup cancelled.`, useColors));
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
const existingConfig = await parseConfigFile(fs, configPath);
|
|
2762
|
+
const config = existingConfig ?? {};
|
|
2763
|
+
if (configExists) {
|
|
2764
|
+
try {
|
|
2765
|
+
await backupConfigFile(fs, configPath, backupPath);
|
|
2766
|
+
console.log(dim(`
|
|
2767
|
+
Backup created: ${backupPath}`, useColors));
|
|
2768
|
+
} catch (backupError) {
|
|
2769
|
+
console.log(error(`Warning: Could not create backup: ${backupError instanceof Error ? backupError.message : String(backupError)}`, useColors));
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
const updatedConfig = addPkgseerToConfig(config);
|
|
2434
2773
|
try {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2774
|
+
const dirPath = fs.getDirname(configPath);
|
|
2775
|
+
await fs.ensureDir(dirPath);
|
|
2776
|
+
} catch (dirError) {
|
|
2777
|
+
console.log(error(`Failed to create directory: ${dirError instanceof Error ? dirError.message : String(dirError)}`, useColors));
|
|
2778
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
try {
|
|
2782
|
+
await writeConfigFile(fs, configPath, updatedConfig);
|
|
2783
|
+
} catch (writeError) {
|
|
2784
|
+
console.log(error(`Failed to write config file: ${writeError instanceof Error ? writeError.message : String(writeError)}`, useColors));
|
|
2785
|
+
showManualInstructions(tool, scope, configPath, useColors);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
const toolNames = {
|
|
2789
|
+
cursor: "Cursor IDE",
|
|
2790
|
+
codex: "Codex CLI",
|
|
2791
|
+
"claude-code": "Claude Code",
|
|
2792
|
+
other: "MCP"
|
|
2793
|
+
};
|
|
2794
|
+
console.log(success(`PkgSeer MCP server configured for ${toolNames[tool]}!`, useColors));
|
|
2795
|
+
console.log(`
|
|
2796
|
+
Config file: ${highlight(configPath, useColors)}`);
|
|
2797
|
+
if (configExists) {
|
|
2798
|
+
console.log(`Backup saved: ${highlight(backupPath, useColors)}`);
|
|
2799
|
+
}
|
|
2800
|
+
console.log(dim(`
|
|
2801
|
+
Next steps:
|
|
2802
|
+
1. Restart your AI assistant to activate the MCP server
|
|
2803
|
+
2. Test by asking your assistant about packages`, useColors));
|
|
2804
|
+
console.log(`
|
|
2805
|
+
Available MCP tools:`);
|
|
2806
|
+
console.log(" • package_summary - Get package overview");
|
|
2807
|
+
console.log(" • package_vulnerabilities - Check for security issues");
|
|
2808
|
+
console.log(" • package_quality - Get quality score");
|
|
2809
|
+
console.log(" • package_dependencies - List dependencies");
|
|
2810
|
+
console.log(" • compare_packages - Compare multiple packages");
|
|
2811
|
+
console.log(" • list_package_docs - List documentation pages");
|
|
2812
|
+
console.log(" • fetch_package_doc - Fetch documentation content");
|
|
2813
|
+
console.log(" • search_package_docs - Search package documentation");
|
|
2814
|
+
if (hasProject) {
|
|
2815
|
+
console.log(" • search_project_docs - Search your project's docs");
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
var MCP_INIT_DESCRIPTION = `Configure PkgSeer's MCP server for your AI assistant.
|
|
2443
2819
|
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2820
|
+
Guides you through:
|
|
2821
|
+
• Selecting your AI tool (Cursor IDE, Codex CLI, Claude Code)
|
|
2822
|
+
• Choosing configuration location (project or global)
|
|
2823
|
+
• Safely editing config files with automatic backup
|
|
2824
|
+
|
|
2825
|
+
Project-level config (recommended):
|
|
2826
|
+
• Uses your project token for authenticated features
|
|
2827
|
+
• Enables search_project_docs (search your project's packages)
|
|
2828
|
+
• Portable with your project`;
|
|
2829
|
+
function registerMcpInitCommand(mcpCommand) {
|
|
2830
|
+
mcpCommand.command("init").summary("Configure MCP server for AI assistants").description(MCP_INIT_DESCRIPTION).action(async () => {
|
|
2448
2831
|
const deps = await createContainer();
|
|
2449
|
-
|
|
2832
|
+
const hasProject = deps.config.project !== undefined;
|
|
2833
|
+
await mcpInitAction({
|
|
2834
|
+
fileSystemService: deps.fileSystemService,
|
|
2835
|
+
promptService: deps.promptService,
|
|
2836
|
+
configService: deps.configService,
|
|
2837
|
+
baseUrl: deps.baseUrl,
|
|
2838
|
+
hasProject
|
|
2839
|
+
});
|
|
2450
2840
|
});
|
|
2451
2841
|
}
|
|
2452
2842
|
|
|
2453
|
-
// src/
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
return {
|
|
2466
|
-
content: [{ type: "text", text }]
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
function errorResult(message) {
|
|
2843
|
+
// src/services/gitignore-parser.ts
|
|
2844
|
+
function parseGitIgnoreLine(line) {
|
|
2845
|
+
const trimmed = line.trimEnd();
|
|
2846
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
const isNegation = trimmed.startsWith("!");
|
|
2850
|
+
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
2851
|
+
const isRootOnly = pattern.startsWith("/");
|
|
2852
|
+
const patternWithoutRoot = isRootOnly ? pattern.slice(1) : pattern;
|
|
2853
|
+
const isDirectory = patternWithoutRoot.endsWith("/");
|
|
2854
|
+
const cleanPattern = isDirectory ? patternWithoutRoot.slice(0, -1) : patternWithoutRoot;
|
|
2470
2855
|
return {
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
// src/tools/shared.ts
|
|
2477
|
-
function toGraphQLRegistry2(registry) {
|
|
2478
|
-
const map = {
|
|
2479
|
-
npm: "NPM",
|
|
2480
|
-
pypi: "PYPI",
|
|
2481
|
-
hex: "HEX"
|
|
2856
|
+
pattern: cleanPattern,
|
|
2857
|
+
isNegation,
|
|
2858
|
+
isDirectory,
|
|
2859
|
+
isRootOnly
|
|
2482
2860
|
};
|
|
2483
|
-
return map[registry.toLowerCase()] || "NPM";
|
|
2484
2861
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2862
|
+
function matchesPattern(path, pattern) {
|
|
2863
|
+
const { pattern: p, isDirectory, isRootOnly } = pattern;
|
|
2864
|
+
let regexStr = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/___DOUBLE_STAR___/g, ".*");
|
|
2865
|
+
if (isRootOnly) {
|
|
2866
|
+
if (isDirectory) {
|
|
2867
|
+
regexStr = `^${regexStr}(/|$)`;
|
|
2868
|
+
} else {
|
|
2869
|
+
regexStr = `^${regexStr}(/|$)`;
|
|
2870
|
+
}
|
|
2871
|
+
} else {
|
|
2872
|
+
if (isDirectory) {
|
|
2873
|
+
regexStr = `(^|/)${regexStr}(/|$)`;
|
|
2874
|
+
} else {
|
|
2875
|
+
regexStr = `(^|/)${regexStr}(/|$)`;
|
|
2876
|
+
}
|
|
2493
2877
|
}
|
|
2494
|
-
|
|
2878
|
+
const regex = new RegExp(regexStr);
|
|
2879
|
+
return regex.test(path);
|
|
2495
2880
|
}
|
|
2496
|
-
async function
|
|
2881
|
+
async function parseGitIgnore(gitignorePath, fs) {
|
|
2497
2882
|
try {
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2883
|
+
const content = await fs.readFile(gitignorePath);
|
|
2884
|
+
const lines = content.split(`
|
|
2885
|
+
`);
|
|
2886
|
+
const patterns = [];
|
|
2887
|
+
for (const line of lines) {
|
|
2888
|
+
const pattern = parseGitIgnoreLine(line);
|
|
2889
|
+
if (pattern) {
|
|
2890
|
+
patterns.push(pattern);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return patterns.length > 0 ? patterns : null;
|
|
2894
|
+
} catch {
|
|
2895
|
+
return null;
|
|
2502
2896
|
}
|
|
2503
2897
|
}
|
|
2504
|
-
function
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
-
});
|
|
2898
|
+
function shouldIgnorePath(relativePath, patterns) {
|
|
2899
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2900
|
+
let ignored = false;
|
|
2901
|
+
for (const pattern of patterns) {
|
|
2902
|
+
if (matchesPattern(normalized, pattern)) {
|
|
2903
|
+
if (pattern.isNegation) {
|
|
2904
|
+
ignored = false;
|
|
2905
|
+
} else {
|
|
2906
|
+
ignored = true;
|
|
2907
|
+
}
|
|
2538
2908
|
}
|
|
2539
|
-
}
|
|
2909
|
+
}
|
|
2910
|
+
return ignored;
|
|
2540
2911
|
}
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
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
|
-
};
|
|
2912
|
+
function shouldIgnoreDirectory(relativeDirPath, patterns) {
|
|
2913
|
+
const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
2914
|
+
return shouldIgnorePath(normalized, patterns);
|
|
2567
2915
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2916
|
+
|
|
2917
|
+
// src/services/manifest-detector.ts
|
|
2918
|
+
var DEFAULT_EXCLUDED_DIRS = [
|
|
2919
|
+
"node_modules",
|
|
2920
|
+
"vendor",
|
|
2921
|
+
".git",
|
|
2922
|
+
"dist",
|
|
2923
|
+
"build",
|
|
2924
|
+
"__pycache__",
|
|
2925
|
+
".venv",
|
|
2926
|
+
"venv",
|
|
2927
|
+
".next"
|
|
2928
|
+
];
|
|
2929
|
+
var MANIFEST_TYPES = [
|
|
2930
|
+
{
|
|
2931
|
+
type: "npm",
|
|
2932
|
+
filenames: [
|
|
2933
|
+
"package.json",
|
|
2934
|
+
"package-lock.json",
|
|
2935
|
+
"yarn.lock",
|
|
2936
|
+
"pnpm-lock.yaml"
|
|
2937
|
+
]
|
|
2938
|
+
},
|
|
2939
|
+
{
|
|
2940
|
+
type: "pypi",
|
|
2941
|
+
filenames: ["requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock"]
|
|
2942
|
+
},
|
|
2943
|
+
{
|
|
2944
|
+
type: "hex",
|
|
2945
|
+
filenames: ["mix.exs", "mix.lock"]
|
|
2946
|
+
}
|
|
2947
|
+
];
|
|
2948
|
+
function suggestLabel(relativePath) {
|
|
2949
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
2950
|
+
const parts = normalized.split("/").filter((p) => p.length > 0);
|
|
2951
|
+
if (parts.length === 1) {
|
|
2952
|
+
return "root";
|
|
2953
|
+
}
|
|
2954
|
+
return parts[parts.length - 2];
|
|
2592
2955
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
return notFoundError(package_name, registry);
|
|
2956
|
+
async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
|
|
2957
|
+
const detected = [];
|
|
2958
|
+
if (currentDepth > options.maxDepth) {
|
|
2959
|
+
return detected;
|
|
2960
|
+
}
|
|
2961
|
+
const relativeDirPath = directory === rootDir ? "." : directory.replace(rootDir, "").replace(/^[/\\]/, "");
|
|
2962
|
+
const baseName = directory.split(/[/\\]/).pop() ?? "";
|
|
2963
|
+
if (options.excludedDirs.includes(baseName)) {
|
|
2964
|
+
return detected;
|
|
2965
|
+
}
|
|
2966
|
+
if (gitignorePatterns && shouldIgnoreDirectory(relativeDirPath, gitignorePatterns)) {
|
|
2967
|
+
return detected;
|
|
2968
|
+
}
|
|
2969
|
+
for (const manifestType of MANIFEST_TYPES) {
|
|
2970
|
+
for (const filename of manifestType.filenames) {
|
|
2971
|
+
const filePath = fs.joinPath(directory, filename);
|
|
2972
|
+
const exists = await fs.exists(filePath);
|
|
2973
|
+
if (exists) {
|
|
2974
|
+
const relativePath = directory === rootDir ? filename : fs.joinPath(directory.replace(rootDir, "").replace(/^[/\\]/, ""), filename);
|
|
2975
|
+
if (gitignorePatterns && shouldIgnorePath(relativePath, gitignorePatterns)) {
|
|
2976
|
+
continue;
|
|
2615
2977
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2978
|
+
detected.push({
|
|
2979
|
+
filename,
|
|
2980
|
+
relativePath,
|
|
2981
|
+
absolutePath: filePath,
|
|
2982
|
+
type: manifestType.type,
|
|
2983
|
+
suggestedLabel: suggestLabel(relativePath)
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2618
2986
|
}
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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);
|
|
2987
|
+
}
|
|
2988
|
+
try {
|
|
2989
|
+
const entries = await fs.readdir(directory);
|
|
2990
|
+
for (const entry of entries) {
|
|
2991
|
+
const entryPath = fs.joinPath(directory, entry);
|
|
2992
|
+
const isDir = await fs.isDirectory(entryPath);
|
|
2993
|
+
if (isDir && !options.excludedDirs.includes(entry)) {
|
|
2994
|
+
const subRelativePath = fs.joinPath(relativeDirPath, entry);
|
|
2995
|
+
if (!gitignorePatterns || !shouldIgnoreDirectory(subRelativePath, gitignorePatterns)) {
|
|
2996
|
+
const subDetected = await scanDirectoryRecursive(entryPath, fs, rootDir, options, currentDepth + 1, gitignorePatterns);
|
|
2997
|
+
detected.push(...subDetected);
|
|
2640
2998
|
}
|
|
2641
|
-
|
|
2642
|
-
});
|
|
2999
|
+
}
|
|
2643
3000
|
}
|
|
3001
|
+
} catch {}
|
|
3002
|
+
return detected;
|
|
3003
|
+
}
|
|
3004
|
+
async function scanForManifests(directory, fs, options) {
|
|
3005
|
+
const opts = {
|
|
3006
|
+
maxDepth: options?.maxDepth ?? 3,
|
|
3007
|
+
excludedDirs: options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS
|
|
2644
3008
|
};
|
|
3009
|
+
const gitignorePath = fs.joinPath(directory, ".gitignore");
|
|
3010
|
+
const gitignorePatterns = await parseGitIgnore(gitignorePath, fs);
|
|
3011
|
+
return scanDirectoryRecursive(directory, fs, directory, opts, 0, gitignorePatterns);
|
|
2645
3012
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
-
});
|
|
3013
|
+
function filterRedundantPackageJson(manifests) {
|
|
3014
|
+
const dirToManifests = new Map;
|
|
3015
|
+
for (const manifest of manifests) {
|
|
3016
|
+
const dir = manifest.relativePath.split(/[/\\]/).slice(0, -1).join("/") || ".";
|
|
3017
|
+
if (!dirToManifests.has(dir)) {
|
|
3018
|
+
dirToManifests.set(dir, []);
|
|
2667
3019
|
}
|
|
2668
|
-
|
|
3020
|
+
dirToManifests.get(dir).push(manifest);
|
|
3021
|
+
}
|
|
3022
|
+
const filtered = [];
|
|
3023
|
+
for (const [dir, dirManifests] of dirToManifests.entries()) {
|
|
3024
|
+
const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
|
|
3025
|
+
const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
|
|
3026
|
+
for (const manifest of dirManifests) {
|
|
3027
|
+
if (manifest.filename === "package.json" && hasLockFile) {
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
filtered.push(manifest);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
return filtered;
|
|
2669
3034
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3035
|
+
async function detectAndGroupManifests(directory, fs, options) {
|
|
3036
|
+
const manifests = await scanForManifests(directory, fs, options);
|
|
3037
|
+
const filteredManifests = filterRedundantPackageJson(manifests);
|
|
3038
|
+
const groupsMap = new Map;
|
|
3039
|
+
for (const manifest of filteredManifests) {
|
|
3040
|
+
const existing = groupsMap.get(manifest.suggestedLabel) ?? [];
|
|
3041
|
+
existing.push(manifest);
|
|
3042
|
+
groupsMap.set(manifest.suggestedLabel, existing);
|
|
3043
|
+
}
|
|
3044
|
+
const groups = [];
|
|
3045
|
+
for (const [label, manifests2] of groupsMap.entries()) {
|
|
3046
|
+
const ecosystems = new Set(manifests2.map((m) => m.type));
|
|
3047
|
+
if (ecosystems.size > 1) {
|
|
3048
|
+
const ecosystemGroups = new Map;
|
|
3049
|
+
for (const manifest of manifests2) {
|
|
3050
|
+
const ecosystemLabel = `${label}-${manifest.type}`;
|
|
3051
|
+
const existing = ecosystemGroups.get(ecosystemLabel) ?? [];
|
|
3052
|
+
existing.push(manifest);
|
|
3053
|
+
ecosystemGroups.set(ecosystemLabel, existing);
|
|
3054
|
+
}
|
|
3055
|
+
for (const [
|
|
3056
|
+
ecosystemLabel,
|
|
3057
|
+
ecosystemManifests
|
|
3058
|
+
] of ecosystemGroups.entries()) {
|
|
3059
|
+
groups.push({ label: ecosystemLabel, manifests: ecosystemManifests });
|
|
3060
|
+
}
|
|
3061
|
+
} else {
|
|
3062
|
+
groups.push({ label, manifests: manifests2 });
|
|
2692
3063
|
}
|
|
2693
|
-
}
|
|
3064
|
+
}
|
|
3065
|
+
groups.sort((a, b) => {
|
|
3066
|
+
if (a.label.startsWith("root")) {
|
|
3067
|
+
if (b.label.startsWith("root")) {
|
|
3068
|
+
return a.label.localeCompare(b.label);
|
|
3069
|
+
}
|
|
3070
|
+
return -1;
|
|
3071
|
+
}
|
|
3072
|
+
if (b.label.startsWith("root")) {
|
|
3073
|
+
return 1;
|
|
3074
|
+
}
|
|
3075
|
+
return a.label.localeCompare(b.label);
|
|
3076
|
+
});
|
|
3077
|
+
return groups;
|
|
2694
3078
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
};
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
-
});
|
|
3079
|
+
|
|
3080
|
+
// src/commands/project/manifest-upload-utils.ts
|
|
3081
|
+
async function processManifestFiles(params) {
|
|
3082
|
+
const {
|
|
3083
|
+
files,
|
|
3084
|
+
basePath,
|
|
3085
|
+
hasHexManifests,
|
|
3086
|
+
allowMixDeps,
|
|
3087
|
+
fileSystemService,
|
|
3088
|
+
shellService
|
|
3089
|
+
} = params;
|
|
3090
|
+
const hexFiles = files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
3091
|
+
let generatedHexFiles = [];
|
|
3092
|
+
if (hasHexManifests && allowMixDeps && hexFiles.length > 0) {
|
|
3093
|
+
const firstHexFile = hexFiles[0];
|
|
3094
|
+
if (!firstHexFile) {
|
|
3095
|
+
throw new Error("No hex files found");
|
|
2739
3096
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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}`);
|
|
3097
|
+
const absolutePath = fileSystemService.joinPath(basePath, firstHexFile);
|
|
3098
|
+
const manifestDir = fileSystemService.getDirname(absolutePath);
|
|
3099
|
+
try {
|
|
3100
|
+
const [depsContent, depsTreeContent] = await Promise.all([
|
|
3101
|
+
shellService.execute("mix deps --all", manifestDir),
|
|
3102
|
+
shellService.execute("mix deps.tree", manifestDir)
|
|
3103
|
+
]);
|
|
3104
|
+
generatedHexFiles = [
|
|
3105
|
+
{
|
|
3106
|
+
filename: "deps.txt",
|
|
3107
|
+
path: absolutePath,
|
|
3108
|
+
content: depsContent
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
filename: "deps-tree.txt",
|
|
3112
|
+
path: absolutePath,
|
|
3113
|
+
content: depsTreeContent
|
|
2777
3114
|
}
|
|
2778
|
-
|
|
2779
|
-
|
|
3115
|
+
];
|
|
3116
|
+
} catch (error2) {
|
|
3117
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
3118
|
+
throw new Error(`Failed to generate dependencies for hex manifest: ${firstHexFile}
|
|
3119
|
+
` + ` Error: ${errorMessage}
|
|
3120
|
+
` + ` Make sure 'mix' is installed and the directory contains a valid Elixir project.`);
|
|
2780
3121
|
}
|
|
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
3122
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
`
|
|
3123
|
+
const filePromises = files.map(async (relativePath) => {
|
|
3124
|
+
const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
|
|
3125
|
+
if (isHexFile) {
|
|
3126
|
+
if (!allowMixDeps) {
|
|
3127
|
+
return null;
|
|
3128
|
+
}
|
|
3129
|
+
return null;
|
|
3130
|
+
}
|
|
3131
|
+
const absolutePath = fileSystemService.joinPath(basePath, relativePath);
|
|
3132
|
+
const exists = await fileSystemService.exists(absolutePath);
|
|
3133
|
+
if (!exists) {
|
|
3134
|
+
throw new Error(`File not found: ${relativePath}
|
|
3135
|
+
Make sure the file exists and the path in pkgseer.yml is correct.`);
|
|
3136
|
+
}
|
|
3137
|
+
const content = await fileSystemService.readFile(absolutePath);
|
|
3138
|
+
const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
|
|
3139
|
+
return {
|
|
3140
|
+
filename,
|
|
3141
|
+
path: absolutePath,
|
|
3142
|
+
content
|
|
3143
|
+
};
|
|
3144
|
+
});
|
|
3145
|
+
const regularFiles = await Promise.all(filePromises);
|
|
3146
|
+
const result = [];
|
|
3147
|
+
let hexFilesInserted = false;
|
|
3148
|
+
for (let i = 0;i < files.length; i++) {
|
|
3149
|
+
const relativePath = files[i];
|
|
3150
|
+
if (!relativePath) {
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
|
|
3154
|
+
if (isHexFile && !hexFilesInserted && generatedHexFiles.length > 0) {
|
|
3155
|
+
result.push(...generatedHexFiles);
|
|
3156
|
+
hexFilesInserted = true;
|
|
3157
|
+
} else if (!isHexFile) {
|
|
3158
|
+
const regularFile = regularFiles[i];
|
|
3159
|
+
if (regularFile !== undefined) {
|
|
3160
|
+
result.push(regularFile);
|
|
3161
|
+
}
|
|
3162
|
+
} else {
|
|
3163
|
+
result.push(null);
|
|
3164
|
+
}
|
|
2832
3165
|
}
|
|
2833
|
-
|
|
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);
|
|
3166
|
+
return result;
|
|
2844
3167
|
}
|
|
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
3168
|
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3169
|
+
// src/commands/project/init.ts
|
|
3170
|
+
async function projectInitAction(options, deps) {
|
|
3171
|
+
const {
|
|
3172
|
+
projectService,
|
|
3173
|
+
configService,
|
|
3174
|
+
fileSystemService,
|
|
3175
|
+
gitService,
|
|
3176
|
+
promptService,
|
|
3177
|
+
shellService,
|
|
3178
|
+
baseUrl
|
|
3179
|
+
} = deps;
|
|
3180
|
+
const useColors = shouldUseColors2();
|
|
3181
|
+
const auth = await checkProjectWriteScope(configService, baseUrl);
|
|
3182
|
+
if (!auth) {
|
|
3183
|
+
console.error(error(`Authentication required with ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope`, useColors));
|
|
3184
|
+
console.log(`
|
|
3185
|
+
Your current token doesn't have the required permissions for creating projects and uploading manifests.`);
|
|
3186
|
+
console.log(`
|
|
3187
|
+
To fix this:`);
|
|
3188
|
+
console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
|
|
3189
|
+
console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
|
|
3190
|
+
console.log(`
|
|
3191
|
+
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
3192
|
+
process.exit(1);
|
|
2854
3193
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
});
|
|
2863
|
-
}
|
|
2864
|
-
|
|
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;
|
|
3194
|
+
const existingConfig = await configService.loadProjectConfig();
|
|
3195
|
+
if (existingConfig?.config.project) {
|
|
3196
|
+
console.error(error(`A project is already configured in pkgseer.yml: ${highlight(existingConfig.config.project, useColors)}`, useColors));
|
|
3197
|
+
console.log(dim(`
|
|
3198
|
+
To reinitialize, either remove pkgseer.yml or edit it manually.`, useColors));
|
|
3199
|
+
console.log(dim(` To update manifest files, use: `, useColors) + highlight(`pkgseer project detect`, useColors));
|
|
3200
|
+
process.exit(1);
|
|
2935
3201
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
vulnerabilities: p.vulnerabilityCount
|
|
2943
|
-
}));
|
|
2944
|
-
output(slim, true);
|
|
2945
|
-
} else {
|
|
2946
|
-
console.log(formatPackageComparison(result.data.comparePackages));
|
|
3202
|
+
let projectName = options.name?.trim();
|
|
3203
|
+
if (!projectName) {
|
|
3204
|
+
const cwd2 = fileSystemService.getCwd();
|
|
3205
|
+
const basename = cwd2.split(/[/\\]/).pop() ?? "project";
|
|
3206
|
+
const input2 = await promptService.input(`Project name:`, basename);
|
|
3207
|
+
projectName = input2.trim();
|
|
2947
3208
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
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);
|
|
3209
|
+
console.log(`
|
|
3210
|
+
Creating project ${highlight(projectName, useColors)}...`);
|
|
3211
|
+
let createResult;
|
|
3212
|
+
let projectAlreadyExists = false;
|
|
3213
|
+
try {
|
|
3214
|
+
createResult = await projectService.createProject({
|
|
3215
|
+
name: projectName
|
|
2968
3216
|
});
|
|
2969
|
-
})
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3217
|
+
} catch (createError) {
|
|
3218
|
+
const errorMessage = createError instanceof Error ? createError.message : String(createError);
|
|
3219
|
+
if (createError instanceof Error && (errorMessage.includes("already been taken") || errorMessage.includes("already exists"))) {
|
|
3220
|
+
console.log(dim(`
|
|
3221
|
+
Project ${highlight(projectName, useColors)} already exists on the server.`, useColors));
|
|
3222
|
+
console.log(dim(` This might happen if you previously ran init but didn't complete the setup.`, useColors));
|
|
3223
|
+
const useExisting = await promptService.confirm(`
|
|
3224
|
+
Do you want to use the existing project and continue with manifest setup?`, true);
|
|
3225
|
+
if (!useExisting) {
|
|
3226
|
+
console.log(dim(`
|
|
3227
|
+
Exiting. Please choose a different project name or use an existing project.`, useColors));
|
|
3228
|
+
process.exit(0);
|
|
3229
|
+
}
|
|
3230
|
+
projectAlreadyExists = true;
|
|
3231
|
+
createResult = {
|
|
3232
|
+
project: {
|
|
3233
|
+
name: projectName,
|
|
3234
|
+
defaultBranch: "main"
|
|
3235
|
+
},
|
|
3236
|
+
errors: null
|
|
3237
|
+
};
|
|
3238
|
+
} else {
|
|
3239
|
+
console.error(error(`Failed to create project: ${errorMessage}`, useColors));
|
|
3240
|
+
if (createError instanceof Error && (errorMessage.includes("Insufficient permissions") || errorMessage.includes("Required scopes") || errorMessage.includes(PROJECT_MANIFEST_UPLOAD_SCOPE))) {
|
|
3241
|
+
console.log(`
|
|
3242
|
+
Your current token doesn't have the required permissions for creating projects.`);
|
|
3243
|
+
console.log(`
|
|
3244
|
+
To fix this:`);
|
|
3245
|
+
console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
|
|
3246
|
+
console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
|
|
3247
|
+
console.log(`
|
|
3248
|
+
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
3249
|
+
} else if (createError instanceof Error && (errorMessage.includes("alphanumeric") || errorMessage.includes("hyphens") || errorMessage.includes("underscores"))) {
|
|
3250
|
+
console.log(dim(`
|
|
3251
|
+
Project name requirements:`, useColors));
|
|
3252
|
+
console.log(dim(` • Must start with an alphanumeric character (a-z, A-Z, 0-9)`, useColors));
|
|
3253
|
+
console.log(dim(` • Can contain letters, numbers, hyphens (-), and underscores (_)`, useColors));
|
|
3254
|
+
console.log(dim(` • Example valid names: ${highlight(`my-project`, useColors)}, ${highlight(`project_123`, useColors)}, ${highlight(`backend`, useColors)}`, useColors));
|
|
3255
|
+
}
|
|
3256
|
+
process.exit(1);
|
|
2986
3257
|
}
|
|
2987
|
-
lines.push("");
|
|
2988
3258
|
}
|
|
2989
|
-
if (
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3259
|
+
if (!createResult.project) {
|
|
3260
|
+
if (createResult.errors && createResult.errors.length > 0) {
|
|
3261
|
+
const firstError = createResult.errors[0];
|
|
3262
|
+
console.error(error(`Failed to create project: ${firstError?.message ?? "Unknown error"}`, useColors));
|
|
3263
|
+
if (firstError?.field) {
|
|
3264
|
+
console.log(dim(`
|
|
3265
|
+
Field: ${firstError.field}`, useColors));
|
|
2995
3266
|
}
|
|
3267
|
+
process.exit(1);
|
|
2996
3268
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
`);
|
|
3002
|
-
}
|
|
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;
|
|
3269
|
+
console.error(error(`Failed to create project`, useColors));
|
|
3270
|
+
console.log(dim(`
|
|
3271
|
+
Please try again or contact support if the issue persists.`, useColors));
|
|
3272
|
+
process.exit(1);
|
|
3011
3273
|
}
|
|
3012
|
-
if (
|
|
3013
|
-
|
|
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);
|
|
3274
|
+
if (projectAlreadyExists) {
|
|
3275
|
+
console.log(success(`Using existing project ${highlight(createResult.project.name, useColors)}`, useColors));
|
|
3024
3276
|
} else {
|
|
3025
|
-
console.log(
|
|
3277
|
+
console.log(success(`Project ${highlight(createResult.project.name, useColors)} created successfully!`, useColors));
|
|
3026
3278
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
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(`
|
|
3279
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
3280
|
+
console.log(dim(`
|
|
3281
|
+
Scanning for manifest files (max depth: ${maxDepth})...`, useColors));
|
|
3282
|
+
const cwd = fileSystemService.getCwd();
|
|
3283
|
+
const manifestGroups = await detectAndGroupManifests(cwd, fileSystemService, {
|
|
3284
|
+
maxDepth
|
|
3285
|
+
});
|
|
3286
|
+
const configToWrite = {
|
|
3287
|
+
project: projectName
|
|
3288
|
+
};
|
|
3289
|
+
if (manifestGroups.length > 0) {
|
|
3290
|
+
console.log(`
|
|
3291
|
+
Found ${highlight(`${manifestGroups.length}`, useColors)} manifest group${manifestGroups.length === 1 ? "" : "s"}:
|
|
3085
3292
|
`);
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3293
|
+
for (const group of manifestGroups) {
|
|
3294
|
+
console.log(` Label: ${highlight(group.label, useColors)}`);
|
|
3295
|
+
for (const manifest of group.manifests) {
|
|
3296
|
+
console.log(` ${highlight(manifest.relativePath, useColors)} ${dim(`(${manifest.type})`, useColors)}`);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
const hasHexManifests = manifestGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
|
|
3300
|
+
let allowMixDeps = false;
|
|
3301
|
+
if (hasHexManifests) {
|
|
3302
|
+
console.log(dim(`
|
|
3303
|
+
Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
|
|
3304
|
+
console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
|
|
3305
|
+
allowMixDeps = await promptService.confirm(`
|
|
3306
|
+
Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
|
|
3307
|
+
}
|
|
3308
|
+
configToWrite.manifests = manifestGroups.map((group) => {
|
|
3309
|
+
const hasHexInGroup = group.manifests.some((m) => m.type === "hex");
|
|
3310
|
+
return {
|
|
3311
|
+
label: group.label,
|
|
3312
|
+
files: group.manifests.map((m) => m.relativePath),
|
|
3313
|
+
...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {}
|
|
3314
|
+
};
|
|
3315
|
+
});
|
|
3316
|
+
const action = await promptService.select(`
|
|
3317
|
+
What would you like to do next?`, [
|
|
3318
|
+
{
|
|
3319
|
+
value: "upload",
|
|
3320
|
+
name: "Save config and upload manifests now",
|
|
3321
|
+
description: "Recommended: saves configuration and uploads files immediately"
|
|
3322
|
+
},
|
|
3323
|
+
{
|
|
3324
|
+
value: "edit",
|
|
3325
|
+
name: "Save config for manual editing",
|
|
3326
|
+
description: "Saves configuration so you can customize labels before uploading"
|
|
3327
|
+
},
|
|
3328
|
+
{
|
|
3329
|
+
value: "abort",
|
|
3330
|
+
name: "Skip configuration for now",
|
|
3331
|
+
description: "Project is created but no config saved (you can run init again later)"
|
|
3332
|
+
}
|
|
3333
|
+
]);
|
|
3334
|
+
if (action === "abort") {
|
|
3335
|
+
if (projectAlreadyExists) {
|
|
3336
|
+
console.log(`
|
|
3337
|
+
${success(`Using existing project ${highlight(projectName, useColors)}`, useColors)}`);
|
|
3338
|
+
} else {
|
|
3339
|
+
console.log(`
|
|
3340
|
+
${success(`Project ${highlight(projectName, useColors)} created successfully!`, useColors)}`);
|
|
3341
|
+
}
|
|
3342
|
+
console.log(dim(`
|
|
3343
|
+
Configuration was not saved. To configure later, run:`, useColors));
|
|
3344
|
+
console.log(` ${highlight(`pkgseer project init --name ${projectName}`, useColors)}`);
|
|
3345
|
+
console.log(dim(`
|
|
3346
|
+
Or manually create pkgseer.yml and run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3347
|
+
process.exit(0);
|
|
3348
|
+
}
|
|
3349
|
+
if (action === "upload") {
|
|
3350
|
+
await configService.writeProjectConfig(configToWrite);
|
|
3351
|
+
console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
|
|
3352
|
+
const branch = await gitService.getCurrentBranch() ?? createResult.project.defaultBranch;
|
|
3353
|
+
console.log(`
|
|
3354
|
+
Uploading manifest files to branch ${highlight(branch, useColors)}...`);
|
|
3355
|
+
const cwd2 = fileSystemService.getCwd();
|
|
3356
|
+
for (const group of manifestGroups) {
|
|
3357
|
+
try {
|
|
3358
|
+
const hasHexManifests2 = group.manifests.some((m) => m.type === "hex");
|
|
3359
|
+
const allowMixDeps2 = configToWrite.manifests?.find((m) => m.label === group.label)?.allow_mix_deps === true;
|
|
3360
|
+
const allFiles = await processManifestFiles({
|
|
3361
|
+
files: group.manifests.map((m) => m.relativePath),
|
|
3362
|
+
basePath: cwd2,
|
|
3363
|
+
hasHexManifests: hasHexManifests2,
|
|
3364
|
+
allowMixDeps: allowMixDeps2 ?? false,
|
|
3365
|
+
fileSystemService,
|
|
3366
|
+
shellService
|
|
3367
|
+
});
|
|
3368
|
+
const validFiles = allFiles.filter((f) => f !== null);
|
|
3369
|
+
if (validFiles.length === 0) {
|
|
3370
|
+
console.log(` ${dim(`(no files to upload for ${group.label})`, useColors)}`);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
const uploadResult = await projectService.uploadManifests({
|
|
3374
|
+
project: projectName,
|
|
3375
|
+
branch,
|
|
3376
|
+
label: group.label,
|
|
3377
|
+
files: validFiles
|
|
3378
|
+
});
|
|
3379
|
+
for (const result of uploadResult.results) {
|
|
3380
|
+
if (result.status === "success") {
|
|
3381
|
+
const depsCount = result.dependencies_count ?? 0;
|
|
3382
|
+
const labelText = highlight(group.label, useColors);
|
|
3383
|
+
const fileText = highlight(result.filename, useColors);
|
|
3384
|
+
const depsText = dim(`(${depsCount} dependencies)`, useColors);
|
|
3385
|
+
console.log(` ${success(`${labelText}: ${fileText}`, useColors)} ${depsText}`);
|
|
3386
|
+
} else {
|
|
3387
|
+
console.log(` ${error(`${group.label}: ${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
} catch (uploadError) {
|
|
3391
|
+
const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
|
|
3392
|
+
let userMessage = `Failed to upload ${group.label}: ${errorMessage}`;
|
|
3393
|
+
if (hasHexManifests) {
|
|
3394
|
+
if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
|
|
3395
|
+
userMessage = `Failed to process hex manifest files for ${group.label}.
|
|
3396
|
+
` + ` Error: ${errorMessage}
|
|
3397
|
+
` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
|
|
3398
|
+
} else if (errorMessage.includes("Failed to generate dependencies")) {
|
|
3399
|
+
userMessage = errorMessage;
|
|
3400
|
+
} else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
|
|
3401
|
+
userMessage = `Failed to upload ${group.label}: Network error.
|
|
3402
|
+
` + ` Error: ${errorMessage}
|
|
3403
|
+
` + ` Check your internet connection and try again.`;
|
|
3404
|
+
}
|
|
3405
|
+
} else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
|
|
3406
|
+
userMessage = `Failed to upload ${group.label}: Network error.
|
|
3407
|
+
` + ` Error: ${errorMessage}
|
|
3408
|
+
` + ` Check your internet connection and try again.`;
|
|
3409
|
+
}
|
|
3410
|
+
console.error(error(userMessage, useColors));
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
console.log(`
|
|
3414
|
+
${success(`Project initialization complete!`, useColors)}`);
|
|
3415
|
+
console.log(dim(`
|
|
3416
|
+
View your project: `, useColors) + highlight(`${deps.baseUrl}/projects/${projectName}`, useColors));
|
|
3417
|
+
console.log(dim(`
|
|
3418
|
+
To upload updated manifests later, run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
} else {
|
|
3422
|
+
console.log(dim(`
|
|
3423
|
+
No manifest files were found in the current directory.`, useColors));
|
|
3095
3424
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
vulnerabilities: data.security?.vulnerabilityCount ?? 0
|
|
3106
|
-
};
|
|
3107
|
-
output(slim, true);
|
|
3425
|
+
await configService.writeProjectConfig(configToWrite);
|
|
3426
|
+
console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
|
|
3427
|
+
if (manifestGroups.length > 0) {
|
|
3428
|
+
console.log(dim(`
|
|
3429
|
+
Next steps:`, useColors));
|
|
3430
|
+
console.log(dim(` 1. Edit pkgseer.yml to customize manifest labels if needed`, useColors));
|
|
3431
|
+
console.log(dim(` 2. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3432
|
+
console.log(dim(`
|
|
3433
|
+
Tip: Use `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` to automatically update your config when you add new manifest files.`, useColors));
|
|
3108
3434
|
} else {
|
|
3109
|
-
console.log(
|
|
3435
|
+
console.log(dim(`
|
|
3436
|
+
Next steps:`, useColors));
|
|
3437
|
+
console.log(dim(` 1. Add manifest files to your project`, useColors));
|
|
3438
|
+
console.log(dim(` 2. Edit pkgseer.yml to configure them:`, useColors));
|
|
3439
|
+
console.log(dim(`
|
|
3440
|
+
manifests:`, useColors));
|
|
3441
|
+
console.log(dim(` - label: backend`, useColors));
|
|
3442
|
+
console.log(dim(` files:`, useColors));
|
|
3443
|
+
console.log(dim(` - `, useColors) + highlight(`package-lock.json`, useColors));
|
|
3444
|
+
console.log(dim(`
|
|
3445
|
+
3. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
|
|
3446
|
+
console.log(dim(`
|
|
3447
|
+
Tip: Run `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` after adding manifest files to auto-configure them.`, useColors));
|
|
3110
3448
|
}
|
|
3111
3449
|
}
|
|
3112
|
-
var
|
|
3450
|
+
var INIT_DESCRIPTION = `Initialize a new project in the current directory.
|
|
3113
3451
|
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3452
|
+
This command will:
|
|
3453
|
+
1. Create a new project entry (or prompt for a project name)
|
|
3454
|
+
2. Scan for manifest files (package.json, requirements.txt, etc.)
|
|
3455
|
+
3. Suggest labels based on directory structure
|
|
3456
|
+
4. Optionally upload manifests to the project
|
|
3119
3457
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
await
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3458
|
+
The project name defaults to the current directory name, or you can
|
|
3459
|
+
specify it with --name. Manifest files are automatically detected
|
|
3460
|
+
and grouped by their directory structure.`;
|
|
3461
|
+
function registerProjectInitCommand(program) {
|
|
3462
|
+
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) => {
|
|
3463
|
+
const deps = await createContainer();
|
|
3464
|
+
await projectInitAction({ name: options.name, maxDepth: options.maxDepth }, {
|
|
3465
|
+
projectService: deps.projectService,
|
|
3466
|
+
configService: deps.configService,
|
|
3467
|
+
fileSystemService: deps.fileSystemService,
|
|
3468
|
+
gitService: deps.gitService,
|
|
3469
|
+
promptService: deps.promptService,
|
|
3470
|
+
shellService: deps.shellService,
|
|
3471
|
+
baseUrl: deps.baseUrl
|
|
3472
|
+
});
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/commands/init.ts
|
|
3477
|
+
async function initAction(options, deps) {
|
|
3478
|
+
const useColors = shouldUseColors2();
|
|
3479
|
+
console.log("Welcome to PkgSeer!");
|
|
3480
|
+
console.log(`───────────────────
|
|
3481
|
+
`);
|
|
3482
|
+
console.log(`Set up PkgSeer for your project:
|
|
3483
|
+
` + ` 1. Project configuration – track dependencies and monitor vulnerabilities
|
|
3484
|
+
` + ` 2. MCP server – enable AI assistant integration
|
|
3485
|
+
`);
|
|
3486
|
+
console.log(dim(`Or run separately: pkgseer project init, pkgseer mcp init
|
|
3487
|
+
`, useColors));
|
|
3488
|
+
let setupProject = !options.skipProject;
|
|
3489
|
+
let setupMcp = !options.skipMcp;
|
|
3490
|
+
if (options.skipProject && options.skipMcp) {
|
|
3491
|
+
showCliUsage(useColors);
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
if (!options.skipProject && !options.skipMcp) {
|
|
3495
|
+
const choice = await deps.promptService.select("What would you like to set up?", [
|
|
3496
|
+
{
|
|
3497
|
+
value: "both",
|
|
3498
|
+
name: "Both project and MCP server (recommended)",
|
|
3499
|
+
description: "Full setup: dependency tracking + AI assistant integration"
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
value: "project",
|
|
3503
|
+
name: "Project only",
|
|
3504
|
+
description: "Track dependencies and monitor for vulnerabilities"
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
value: "mcp",
|
|
3508
|
+
name: "MCP server only",
|
|
3509
|
+
description: "Enable AI assistant integration"
|
|
3510
|
+
},
|
|
3511
|
+
{
|
|
3512
|
+
value: "cli",
|
|
3513
|
+
name: "CLI usage (no setup)",
|
|
3514
|
+
description: "Use PkgSeer as a command-line tool"
|
|
3147
3515
|
}
|
|
3516
|
+
]);
|
|
3517
|
+
if (choice === "cli") {
|
|
3518
|
+
showCliUsage(useColors);
|
|
3519
|
+
return;
|
|
3148
3520
|
}
|
|
3149
|
-
|
|
3521
|
+
setupProject = choice === "both" || choice === "project";
|
|
3522
|
+
setupMcp = choice === "both" || choice === "mcp";
|
|
3150
3523
|
}
|
|
3151
|
-
|
|
3524
|
+
const projectInit = deps.projectInitAction ?? projectInitAction;
|
|
3525
|
+
const mcpInit = deps.mcpInitAction ?? mcpInitAction;
|
|
3526
|
+
if (setupProject) {
|
|
3527
|
+
const existingConfig = await deps.configService.loadProjectConfig();
|
|
3528
|
+
if (existingConfig?.config.project) {
|
|
3529
|
+
console.log(`
|
|
3530
|
+
Project already configured: ${highlight(existingConfig.config.project, useColors)}`);
|
|
3531
|
+
console.log(dim(`Skipping project setup. Run 'pkgseer project init' to reinitialize.
|
|
3532
|
+
`, useColors));
|
|
3533
|
+
setupProject = false;
|
|
3534
|
+
} else {
|
|
3535
|
+
console.log(`
|
|
3536
|
+
` + "=".repeat(50));
|
|
3537
|
+
console.log("Project Configuration Setup");
|
|
3538
|
+
console.log("=".repeat(50) + `
|
|
3152
3539
|
`);
|
|
3153
|
-
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3540
|
+
await projectInit({}, {
|
|
3541
|
+
projectService: deps.projectService,
|
|
3542
|
+
configService: deps.configService,
|
|
3543
|
+
fileSystemService: deps.fileSystemService,
|
|
3544
|
+
gitService: deps.gitService,
|
|
3545
|
+
promptService: deps.promptService,
|
|
3546
|
+
shellService: deps.shellService,
|
|
3547
|
+
baseUrl: deps.baseUrl
|
|
3548
|
+
});
|
|
3549
|
+
const updatedConfig = await deps.configService.loadProjectConfig();
|
|
3550
|
+
if (updatedConfig?.config.project) {
|
|
3551
|
+
console.log(`
|
|
3552
|
+
${highlight("✓", useColors)} Project setup complete!
|
|
3553
|
+
`);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3162
3556
|
}
|
|
3163
|
-
if (
|
|
3164
|
-
const
|
|
3165
|
-
const
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3557
|
+
if (setupMcp) {
|
|
3558
|
+
const currentConfig = await deps.configService.loadProjectConfig();
|
|
3559
|
+
const hasProjectNow = currentConfig?.config.project !== undefined;
|
|
3560
|
+
console.log(`
|
|
3561
|
+
` + "=".repeat(50));
|
|
3562
|
+
console.log("MCP Server Setup");
|
|
3563
|
+
console.log("=".repeat(50) + `
|
|
3564
|
+
`);
|
|
3565
|
+
await mcpInit({
|
|
3566
|
+
fileSystemService: deps.fileSystemService,
|
|
3567
|
+
promptService: deps.promptService,
|
|
3568
|
+
configService: deps.configService,
|
|
3569
|
+
baseUrl: deps.baseUrl,
|
|
3570
|
+
hasProject: hasProjectNow
|
|
3571
|
+
});
|
|
3572
|
+
console.log(`
|
|
3573
|
+
${highlight("✓", useColors)} MCP setup complete!
|
|
3574
|
+
`);
|
|
3575
|
+
}
|
|
3576
|
+
console.log("=".repeat(50));
|
|
3577
|
+
console.log("Setup Complete!");
|
|
3578
|
+
console.log("=".repeat(50) + `
|
|
3579
|
+
`);
|
|
3580
|
+
if (setupProject) {
|
|
3581
|
+
const finalConfig = await deps.configService.loadProjectConfig();
|
|
3582
|
+
if (finalConfig?.config.project) {
|
|
3583
|
+
console.log(`Project: ${highlight(finalConfig.config.project, useColors)}`);
|
|
3584
|
+
console.log(dim(` View at: ${deps.baseUrl}/projects/${finalConfig.config.project}`, useColors));
|
|
3585
|
+
}
|
|
3177
3586
|
}
|
|
3587
|
+
if (setupMcp) {
|
|
3588
|
+
console.log("MCP Server: Configured");
|
|
3589
|
+
console.log(dim(" Restart your AI assistant to activate the MCP server.", useColors));
|
|
3590
|
+
}
|
|
3591
|
+
console.log(dim(`
|
|
3592
|
+
Next steps:
|
|
3593
|
+
` + ` • Use CLI commands: pkgseer pkg info <package>
|
|
3594
|
+
` + ` • Search docs: pkgseer docs search <query>
|
|
3595
|
+
` + " • Quick reference: pkgseer quickstart", useColors));
|
|
3596
|
+
}
|
|
3597
|
+
function showCliUsage(useColors) {
|
|
3598
|
+
console.log("Using PkgSeer via CLI");
|
|
3599
|
+
console.log(`─────────────────────
|
|
3600
|
+
`);
|
|
3601
|
+
console.log(`PkgSeer works great as a standalone CLI tool. Your AI assistant
|
|
3602
|
+
` + `can run these commands directly:
|
|
3603
|
+
`);
|
|
3604
|
+
console.log("Package Commands:");
|
|
3605
|
+
console.log(` ${highlight("pkgseer pkg info <package>", useColors)} Get package summary`);
|
|
3606
|
+
console.log(` ${highlight("pkgseer pkg vulns <package>", useColors)} Check vulnerabilities`);
|
|
3607
|
+
console.log(` ${highlight("pkgseer pkg quality <package>", useColors)} Get quality score`);
|
|
3608
|
+
console.log(` ${highlight("pkgseer pkg deps <package>", useColors)} List dependencies`);
|
|
3609
|
+
console.log(` ${highlight("pkgseer pkg compare <pkg...>", useColors)} Compare packages
|
|
3610
|
+
`);
|
|
3611
|
+
console.log("Documentation Commands:");
|
|
3612
|
+
console.log(` ${highlight("pkgseer docs list <package>", useColors)} List doc pages`);
|
|
3613
|
+
console.log(` ${highlight("pkgseer docs get <pkg>/<page>", useColors)} Fetch doc content`);
|
|
3614
|
+
console.log(` ${highlight("pkgseer docs search <query>", useColors)} Search documentation
|
|
3615
|
+
`);
|
|
3616
|
+
console.log(dim(`All commands support --json for structured output.
|
|
3617
|
+
` + "Tip: Run 'pkgseer quickstart' for a quick reference guide.", useColors));
|
|
3178
3618
|
}
|
|
3179
|
-
|
|
3619
|
+
function registerInitCommand(program) {
|
|
3620
|
+
program.command("init").summary("Set up project and MCP server").description(`Set up PkgSeer for your project.
|
|
3180
3621
|
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
- Security practices
|
|
3185
|
-
- Community engagement
|
|
3622
|
+
Guides you through:
|
|
3623
|
+
• Project configuration – track dependencies, monitor vulnerabilities
|
|
3624
|
+
• MCP server – enable AI assistant integration
|
|
3186
3625
|
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
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
|
-
});
|
|
3626
|
+
Run separately: pkgseer project init, pkgseer mcp init`).option("--skip-project", "Skip project setup").option("--skip-mcp", "Skip MCP setup").action(async (options) => {
|
|
3627
|
+
const deps = await createContainer();
|
|
3628
|
+
await initAction(options, deps);
|
|
3197
3629
|
});
|
|
3198
3630
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
if (score >= 7)
|
|
3206
|
-
return "HIGH";
|
|
3207
|
-
if (score >= 4)
|
|
3208
|
-
return "MEDIUM";
|
|
3209
|
-
return "LOW";
|
|
3631
|
+
|
|
3632
|
+
// src/commands/login.ts
|
|
3633
|
+
import { hostname } from "node:os";
|
|
3634
|
+
var TIMEOUT_MS = 5 * 60 * 1000;
|
|
3635
|
+
function randomPort() {
|
|
3636
|
+
return Math.floor(Math.random() * 2000) + 8000;
|
|
3210
3637
|
}
|
|
3211
|
-
function
|
|
3212
|
-
const
|
|
3213
|
-
const
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3638
|
+
async function loginAction(options, deps) {
|
|
3639
|
+
const { authService, authStorage, browserService, baseUrl } = deps;
|
|
3640
|
+
const existing = await authStorage.load(baseUrl);
|
|
3641
|
+
if (existing && !options.force) {
|
|
3642
|
+
const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
|
|
3643
|
+
if (!isExpired) {
|
|
3644
|
+
console.log(`Already logged in.
|
|
3645
|
+
`);
|
|
3646
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3647
|
+
console.log(` Token: ${existing.tokenName}
|
|
3648
|
+
`);
|
|
3649
|
+
console.log("To switch accounts, run `pkgseer logout` first.");
|
|
3650
|
+
console.log("To re-authenticate with different scopes, use `pkgseer login --force`.");
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
console.log(`Token expired. Starting new login...
|
|
3654
|
+
`);
|
|
3655
|
+
} else if (existing && options.force) {
|
|
3656
|
+
console.log(`Re-authenticating (--force flag)...
|
|
3657
|
+
`);
|
|
3217
3658
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3659
|
+
const { verifier, challenge, state } = authService.generatePkceParams();
|
|
3660
|
+
const port = options.port ?? randomPort();
|
|
3661
|
+
const authUrl = authService.buildAuthUrl({
|
|
3662
|
+
state,
|
|
3663
|
+
port,
|
|
3664
|
+
codeChallenge: challenge,
|
|
3665
|
+
hostname: hostname()
|
|
3666
|
+
});
|
|
3667
|
+
const serverPromise = authService.startCallbackServer(port);
|
|
3668
|
+
if (options.browser === false) {
|
|
3669
|
+
console.log(`Open this URL in your browser:
|
|
3670
|
+
`);
|
|
3671
|
+
console.log(` ${authUrl}
|
|
3223
3672
|
`);
|
|
3673
|
+
} else {
|
|
3674
|
+
console.log("Opening browser...");
|
|
3675
|
+
await browserService.open(authUrl);
|
|
3224
3676
|
}
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3677
|
+
console.log(`Waiting for authentication...
|
|
3678
|
+
`);
|
|
3679
|
+
let timeoutId;
|
|
3680
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3681
|
+
timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
|
|
3682
|
+
});
|
|
3683
|
+
let callback;
|
|
3684
|
+
try {
|
|
3685
|
+
callback = await Promise.race([serverPromise, timeoutPromise]);
|
|
3686
|
+
clearTimeout(timeoutId);
|
|
3687
|
+
} catch (error2) {
|
|
3688
|
+
clearTimeout(timeoutId);
|
|
3689
|
+
if (error2 instanceof Error) {
|
|
3690
|
+
console.log(`${error2.message}.
|
|
3691
|
+
`);
|
|
3692
|
+
console.log("Run `pkgseer login` to try again.");
|
|
3229
3693
|
}
|
|
3230
|
-
|
|
3231
|
-
}, {});
|
|
3232
|
-
lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
|
|
3233
|
-
for (const [severity, count] of Object.entries(bySeverity)) {
|
|
3234
|
-
lines.push(` ${formatSeverity(severity)}: ${count}`);
|
|
3694
|
+
process.exit(1);
|
|
3235
3695
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3696
|
+
if (callback.state !== state) {
|
|
3697
|
+
console.error(`Security error: authentication state mismatch.
|
|
3698
|
+
`);
|
|
3699
|
+
console.log("This could indicate a security issue. Please try again.");
|
|
3700
|
+
process.exit(1);
|
|
3701
|
+
}
|
|
3702
|
+
let tokenResponse;
|
|
3703
|
+
try {
|
|
3704
|
+
tokenResponse = await authService.exchangeCodeForToken({
|
|
3705
|
+
code: callback.code,
|
|
3706
|
+
codeVerifier: verifier,
|
|
3707
|
+
state
|
|
3708
|
+
});
|
|
3709
|
+
} catch (error2) {
|
|
3710
|
+
console.error(`Failed to complete authentication: ${error2 instanceof Error ? error2.message : error2}
|
|
3711
|
+
`);
|
|
3712
|
+
console.log("Run `pkgseer login` to try again.");
|
|
3713
|
+
process.exit(1);
|
|
3714
|
+
}
|
|
3715
|
+
await authStorage.save(baseUrl, {
|
|
3716
|
+
token: tokenResponse.token,
|
|
3717
|
+
tokenName: tokenResponse.tokenName,
|
|
3718
|
+
scopes: tokenResponse.scopes,
|
|
3719
|
+
createdAt: new Date().toISOString(),
|
|
3720
|
+
expiresAt: tokenResponse.expiresAt,
|
|
3721
|
+
apiKeyId: tokenResponse.apiKeyId
|
|
3722
|
+
});
|
|
3723
|
+
console.log(`✓ Logged in
|
|
3724
|
+
`);
|
|
3725
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3726
|
+
console.log(` Token: ${tokenResponse.tokenName}`);
|
|
3727
|
+
if (tokenResponse.expiresAt) {
|
|
3728
|
+
const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
3729
|
+
console.log(` Expires: in ${days} days`);
|
|
3730
|
+
}
|
|
3731
|
+
console.log(`
|
|
3732
|
+
You're ready to use pkgseer with your AI assistant.`);
|
|
3733
|
+
}
|
|
3734
|
+
var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
|
|
3735
|
+
|
|
3736
|
+
Opens your browser to complete authentication securely. The CLI receives
|
|
3737
|
+
a token that's stored locally and used for API requests.
|
|
3738
|
+
|
|
3739
|
+
Use --no-browser in environments without a display (CI, SSH sessions)
|
|
3740
|
+
to get a URL you can open on another device.`;
|
|
3741
|
+
function registerLoginCommand(program) {
|
|
3742
|
+
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) => {
|
|
3743
|
+
const deps = await createContainer();
|
|
3744
|
+
await loginAction(options, deps);
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
// src/commands/logout.ts
|
|
3749
|
+
async function logoutAction(deps) {
|
|
3750
|
+
const { authService, authStorage, baseUrl } = deps;
|
|
3751
|
+
const auth = await authStorage.load(baseUrl);
|
|
3752
|
+
if (!auth) {
|
|
3753
|
+
console.log(`Not currently logged in.
|
|
3754
|
+
`);
|
|
3755
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
try {
|
|
3759
|
+
await authService.revokeToken(auth.token);
|
|
3760
|
+
} catch {}
|
|
3761
|
+
await authStorage.clear(baseUrl);
|
|
3762
|
+
console.log(`✓ Logged out
|
|
3763
|
+
`);
|
|
3764
|
+
console.log(` Environment: ${baseUrl}`);
|
|
3765
|
+
}
|
|
3766
|
+
var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
|
|
3767
|
+
|
|
3768
|
+
Clears the locally stored authentication token and notifies the server
|
|
3769
|
+
to revoke it. Use this when switching accounts or on shared machines.`;
|
|
3770
|
+
function registerLogoutCommand(program) {
|
|
3771
|
+
program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
|
|
3772
|
+
const deps = await createContainer();
|
|
3773
|
+
await logoutAction(deps);
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/commands/mcp.ts
|
|
3778
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3779
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3780
|
+
|
|
3781
|
+
// src/tools/compare-packages.ts
|
|
3782
|
+
import { z as z3 } from "zod";
|
|
3783
|
+
|
|
3784
|
+
// src/tools/shared.ts
|
|
3785
|
+
import { z as z2 } from "zod";
|
|
3786
|
+
|
|
3787
|
+
// src/tools/types.ts
|
|
3788
|
+
function textResult(text) {
|
|
3789
|
+
return {
|
|
3790
|
+
content: [{ type: "text", text }]
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
function errorResult(message) {
|
|
3794
|
+
return {
|
|
3795
|
+
content: [{ type: "text", text: message }],
|
|
3796
|
+
isError: true
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
// src/tools/shared.ts
|
|
3801
|
+
function toGraphQLRegistry2(registry) {
|
|
3802
|
+
const map = {
|
|
3803
|
+
npm: "NPM",
|
|
3804
|
+
pypi: "PYPI",
|
|
3805
|
+
hex: "HEX"
|
|
3806
|
+
};
|
|
3807
|
+
return map[registry.toLowerCase()] || "NPM";
|
|
3808
|
+
}
|
|
3809
|
+
var schemas = {
|
|
3810
|
+
registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
|
|
3811
|
+
packageName: z2.string().max(255).describe("Name of the package"),
|
|
3812
|
+
version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
|
|
3813
|
+
};
|
|
3814
|
+
function handleGraphQLErrors(errors) {
|
|
3815
|
+
if (errors && errors.length > 0) {
|
|
3816
|
+
return errorResult(`Error: ${errors.map((e) => e.message).join(", ")}`);
|
|
3817
|
+
}
|
|
3818
|
+
return null;
|
|
3819
|
+
}
|
|
3820
|
+
async function withErrorHandling(operation, fn) {
|
|
3821
|
+
try {
|
|
3822
|
+
return await fn();
|
|
3823
|
+
} catch (error2) {
|
|
3824
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3825
|
+
return errorResult(`Failed to ${operation}: ${message}`);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
function notFoundError(packageName, registry) {
|
|
3829
|
+
return errorResult(`Package not found: ${packageName} in ${registry}`);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
// src/tools/compare-packages.ts
|
|
3833
|
+
var packageInputSchema = z3.object({
|
|
3834
|
+
registry: z3.enum(["npm", "pypi", "hex"]),
|
|
3835
|
+
name: z3.string().max(255),
|
|
3836
|
+
version: z3.string().max(100).optional()
|
|
3837
|
+
});
|
|
3838
|
+
var argsSchema = {
|
|
3839
|
+
packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
|
|
3840
|
+
};
|
|
3841
|
+
function createComparePackagesTool(pkgseerService) {
|
|
3842
|
+
return {
|
|
3843
|
+
name: "compare_packages",
|
|
3844
|
+
description: "Compares multiple packages across metadata, quality, and security dimensions",
|
|
3845
|
+
schema: argsSchema,
|
|
3846
|
+
handler: async ({ packages }, _extra) => {
|
|
3847
|
+
return withErrorHandling("compare packages", async () => {
|
|
3848
|
+
const input2 = packages.map((pkg) => ({
|
|
3849
|
+
registry: toGraphQLRegistry2(pkg.registry),
|
|
3850
|
+
name: pkg.name,
|
|
3851
|
+
version: pkg.version
|
|
3852
|
+
}));
|
|
3853
|
+
const result = await pkgseerService.comparePackages(input2);
|
|
3854
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3855
|
+
if (graphqlError)
|
|
3856
|
+
return graphqlError;
|
|
3857
|
+
if (!result.data.comparePackages) {
|
|
3858
|
+
return errorResult("Comparison failed: no results returned");
|
|
3859
|
+
}
|
|
3860
|
+
return textResult(JSON.stringify(result.data.comparePackages, null, 2));
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
// src/tools/fetch-package-doc.ts
|
|
3866
|
+
import { z as z4 } from "zod";
|
|
3867
|
+
var argsSchema2 = {
|
|
3868
|
+
page_id: z4.string().max(500).describe("Globally unique documentation page identifier (from list_package_docs)")
|
|
3869
|
+
};
|
|
3870
|
+
function createFetchPackageDocTool(pkgseerService) {
|
|
3871
|
+
return {
|
|
3872
|
+
name: "fetch_package_doc",
|
|
3873
|
+
description: "Fetches the full content of a specific documentation page using a globally unique page ID. Returns complete page metadata including title, content (markdown/HTML), content format, breadcrumbs, source attribution, link metadata, base URL, and last updated timestamp. Use list_package_docs first to get available page IDs.",
|
|
3874
|
+
schema: argsSchema2,
|
|
3875
|
+
handler: async ({ page_id }, _extra) => {
|
|
3876
|
+
return withErrorHandling("fetch documentation page", async () => {
|
|
3877
|
+
const result = await pkgseerService.getDocPage(page_id);
|
|
3878
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3879
|
+
if (graphqlError)
|
|
3880
|
+
return graphqlError;
|
|
3881
|
+
if (!result.data.getDocPage) {
|
|
3882
|
+
return errorResult(`Documentation page not found: ${page_id}`);
|
|
3883
|
+
}
|
|
3884
|
+
return textResult(JSON.stringify(result.data.getDocPage, null, 2));
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
// src/tools/list-package-docs.ts
|
|
3890
|
+
var argsSchema3 = {
|
|
3891
|
+
registry: schemas.registry,
|
|
3892
|
+
package_name: schemas.packageName.describe("Name of the package to list documentation for"),
|
|
3893
|
+
version: schemas.version
|
|
3894
|
+
};
|
|
3895
|
+
function createListPackageDocsTool(pkgseerService) {
|
|
3896
|
+
return {
|
|
3897
|
+
name: "list_package_docs",
|
|
3898
|
+
description: "Lists documentation pages for a package version. Returns page titles, globally unique IDs, and metadata. Use this to discover available documentation before fetching specific pages.",
|
|
3899
|
+
schema: argsSchema3,
|
|
3900
|
+
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
3901
|
+
return withErrorHandling("list package documentation", async () => {
|
|
3902
|
+
const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
|
|
3903
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3904
|
+
if (graphqlError)
|
|
3905
|
+
return graphqlError;
|
|
3906
|
+
if (!result.data.listPackageDocs) {
|
|
3907
|
+
return notFoundError(package_name, registry);
|
|
3908
|
+
}
|
|
3909
|
+
return textResult(JSON.stringify(result.data.listPackageDocs, null, 2));
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
// src/tools/package-dependencies.ts
|
|
3915
|
+
import { z as z5 } from "zod";
|
|
3916
|
+
var argsSchema4 = {
|
|
3917
|
+
registry: schemas.registry,
|
|
3918
|
+
package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
|
|
3919
|
+
version: schemas.version,
|
|
3920
|
+
include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
|
|
3921
|
+
max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
|
|
3922
|
+
};
|
|
3923
|
+
function createPackageDependenciesTool(pkgseerService) {
|
|
3924
|
+
return {
|
|
3925
|
+
name: "package_dependencies",
|
|
3926
|
+
description: "Retrieves direct and transitive dependencies for a package version",
|
|
3927
|
+
schema: argsSchema4,
|
|
3928
|
+
handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
|
|
3929
|
+
return withErrorHandling("fetch package dependencies", async () => {
|
|
3930
|
+
const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
|
|
3931
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3932
|
+
if (graphqlError)
|
|
3933
|
+
return graphqlError;
|
|
3934
|
+
if (!result.data.packageDependencies) {
|
|
3935
|
+
return notFoundError(package_name, registry);
|
|
3936
|
+
}
|
|
3937
|
+
return textResult(JSON.stringify(result.data.packageDependencies, null, 2));
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
// src/tools/package-quality.ts
|
|
3943
|
+
var argsSchema5 = {
|
|
3944
|
+
registry: schemas.registry,
|
|
3945
|
+
package_name: schemas.packageName.describe("Name of the package to analyze"),
|
|
3946
|
+
version: schemas.version
|
|
3947
|
+
};
|
|
3948
|
+
function createPackageQualityTool(pkgseerService) {
|
|
3949
|
+
return {
|
|
3950
|
+
name: "package_quality",
|
|
3951
|
+
description: "Retrieves quality score and rule-level breakdown for a package",
|
|
3952
|
+
schema: argsSchema5,
|
|
3953
|
+
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
3954
|
+
return withErrorHandling("fetch package quality", async () => {
|
|
3955
|
+
const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
|
|
3956
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3957
|
+
if (graphqlError)
|
|
3958
|
+
return graphqlError;
|
|
3959
|
+
if (!result.data.packageQuality) {
|
|
3960
|
+
return notFoundError(package_name, registry);
|
|
3961
|
+
}
|
|
3962
|
+
return textResult(JSON.stringify(result.data.packageQuality, null, 2));
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
// src/tools/package-summary.ts
|
|
3968
|
+
var argsSchema6 = {
|
|
3969
|
+
registry: schemas.registry,
|
|
3970
|
+
package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
|
|
3971
|
+
};
|
|
3972
|
+
function createPackageSummaryTool(pkgseerService) {
|
|
3973
|
+
return {
|
|
3974
|
+
name: "package_summary",
|
|
3975
|
+
description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
|
|
3976
|
+
schema: argsSchema6,
|
|
3977
|
+
handler: async ({ registry, package_name }, _extra) => {
|
|
3978
|
+
return withErrorHandling("fetch package summary", async () => {
|
|
3979
|
+
const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
|
|
3980
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
3981
|
+
if (graphqlError)
|
|
3982
|
+
return graphqlError;
|
|
3983
|
+
if (!result.data.packageSummary) {
|
|
3984
|
+
return notFoundError(package_name, registry);
|
|
3985
|
+
}
|
|
3986
|
+
return textResult(JSON.stringify(result.data.packageSummary, null, 2));
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
// src/tools/package-vulnerabilities.ts
|
|
3992
|
+
var argsSchema7 = {
|
|
3993
|
+
registry: schemas.registry,
|
|
3994
|
+
package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
|
|
3995
|
+
version: schemas.version
|
|
3996
|
+
};
|
|
3997
|
+
function createPackageVulnerabilitiesTool(pkgseerService) {
|
|
3998
|
+
return {
|
|
3999
|
+
name: "package_vulnerabilities",
|
|
4000
|
+
description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
|
|
4001
|
+
schema: argsSchema7,
|
|
4002
|
+
handler: async ({ registry, package_name, version: version2 }, _extra) => {
|
|
4003
|
+
return withErrorHandling("fetch package vulnerabilities", async () => {
|
|
4004
|
+
const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
|
|
4005
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
4006
|
+
if (graphqlError)
|
|
4007
|
+
return graphqlError;
|
|
4008
|
+
if (!result.data.packageVulnerabilities) {
|
|
4009
|
+
return notFoundError(package_name, registry);
|
|
4010
|
+
}
|
|
4011
|
+
return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
// src/tools/search-package-docs.ts
|
|
4017
|
+
import { z as z6 } from "zod";
|
|
4018
|
+
var argsSchema8 = {
|
|
4019
|
+
registry: schemas.registry,
|
|
4020
|
+
package_name: schemas.packageName.describe("Name of the package to search documentation for"),
|
|
4021
|
+
keywords: z6.array(z6.string()).optional().describe("Keywords to search for; combined into a single query"),
|
|
4022
|
+
query: z6.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
|
|
4023
|
+
include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
|
|
4024
|
+
limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
|
|
4025
|
+
version: schemas.version
|
|
4026
|
+
};
|
|
4027
|
+
function createSearchPackageDocsTool(pkgseerService) {
|
|
4028
|
+
return {
|
|
4029
|
+
name: "search_package_docs",
|
|
4030
|
+
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.",
|
|
4031
|
+
schema: argsSchema8,
|
|
4032
|
+
handler: async ({
|
|
4033
|
+
registry,
|
|
4034
|
+
package_name,
|
|
4035
|
+
keywords,
|
|
4036
|
+
query,
|
|
4037
|
+
include_snippets,
|
|
4038
|
+
limit,
|
|
4039
|
+
version: version2
|
|
4040
|
+
}, _extra) => {
|
|
4041
|
+
return withErrorHandling("search package documentation", async () => {
|
|
4042
|
+
if (!keywords?.length && !query) {
|
|
4043
|
+
return errorResult("Either keywords or query must be provided for search");
|
|
4044
|
+
}
|
|
4045
|
+
const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
|
|
4046
|
+
keywords,
|
|
4047
|
+
query,
|
|
4048
|
+
includeSnippets: include_snippets,
|
|
4049
|
+
limit,
|
|
4050
|
+
version: version2
|
|
4051
|
+
});
|
|
4052
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
4053
|
+
if (graphqlError)
|
|
4054
|
+
return graphqlError;
|
|
4055
|
+
if (!result.data.searchPackageDocs) {
|
|
4056
|
+
return errorResult(`No documentation found for ${package_name} in ${registry}`);
|
|
4057
|
+
}
|
|
4058
|
+
return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
// src/tools/search-project-docs.ts
|
|
4064
|
+
import { z as z7 } from "zod";
|
|
4065
|
+
var argsSchema9 = {
|
|
4066
|
+
project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
|
|
4067
|
+
keywords: z7.array(z7.string()).optional().describe("Keywords to search for; combined into a single query"),
|
|
4068
|
+
query: z7.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
|
|
4069
|
+
include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
|
|
4070
|
+
limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
|
|
4071
|
+
};
|
|
4072
|
+
function createSearchProjectDocsTool(deps) {
|
|
4073
|
+
const { pkgseerService, config } = deps;
|
|
4074
|
+
return {
|
|
4075
|
+
name: "search_project_docs",
|
|
4076
|
+
description: "Searches documentation across all dependencies in a PkgSeer project. Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
|
|
4077
|
+
schema: argsSchema9,
|
|
4078
|
+
handler: async ({ project, keywords, query, include_snippets, limit }, _extra) => {
|
|
4079
|
+
return withErrorHandling("search project documentation", async () => {
|
|
4080
|
+
const resolvedProject = project ?? config.project;
|
|
4081
|
+
if (!resolvedProject) {
|
|
4082
|
+
return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
|
|
4083
|
+
}
|
|
4084
|
+
if (!keywords?.length && !query) {
|
|
4085
|
+
return errorResult("Either keywords or query must be provided for search");
|
|
4086
|
+
}
|
|
4087
|
+
const result = await pkgseerService.searchProjectDocs(resolvedProject, {
|
|
4088
|
+
keywords,
|
|
4089
|
+
query,
|
|
4090
|
+
includeSnippets: include_snippets,
|
|
4091
|
+
limit
|
|
4092
|
+
});
|
|
4093
|
+
const graphqlError = handleGraphQLErrors(result.errors);
|
|
4094
|
+
if (graphqlError)
|
|
4095
|
+
return graphqlError;
|
|
4096
|
+
if (!result.data.searchProjectDocs) {
|
|
4097
|
+
return errorResult(`Project not found: ${resolvedProject}`);
|
|
4098
|
+
}
|
|
4099
|
+
return textResult(JSON.stringify(result.data.searchProjectDocs, null, 2));
|
|
4100
|
+
});
|
|
3247
4101
|
}
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
// src/commands/mcp.ts
|
|
4105
|
+
var TOOL_FACTORIES = {
|
|
4106
|
+
package_summary: ({ pkgseerService }) => createPackageSummaryTool(pkgseerService),
|
|
4107
|
+
package_vulnerabilities: ({ pkgseerService }) => createPackageVulnerabilitiesTool(pkgseerService),
|
|
4108
|
+
package_dependencies: ({ pkgseerService }) => createPackageDependenciesTool(pkgseerService),
|
|
4109
|
+
package_quality: ({ pkgseerService }) => createPackageQualityTool(pkgseerService),
|
|
4110
|
+
compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
|
|
4111
|
+
list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
|
|
4112
|
+
fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
|
|
4113
|
+
search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
|
|
4114
|
+
search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
|
|
4115
|
+
};
|
|
4116
|
+
var PUBLIC_READ_TOOLS = [
|
|
4117
|
+
"package_summary",
|
|
4118
|
+
"package_vulnerabilities",
|
|
4119
|
+
"package_dependencies",
|
|
4120
|
+
"package_quality",
|
|
4121
|
+
"compare_packages",
|
|
4122
|
+
"list_package_docs",
|
|
4123
|
+
"fetch_package_doc",
|
|
4124
|
+
"search_package_docs"
|
|
4125
|
+
];
|
|
4126
|
+
var PROJECT_READ_TOOLS = ["search_project_docs"];
|
|
4127
|
+
var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
|
|
4128
|
+
function createMcpServer(deps) {
|
|
4129
|
+
const { pkgseerService, config } = deps;
|
|
4130
|
+
const server = new McpServer({
|
|
4131
|
+
name: "pkgseer",
|
|
4132
|
+
version: "0.1.0"
|
|
4133
|
+
});
|
|
4134
|
+
const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
|
|
4135
|
+
const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
|
|
4136
|
+
for (const toolName of toolsToRegister) {
|
|
4137
|
+
const factory = TOOL_FACTORIES[toolName];
|
|
4138
|
+
const tool = factory({ pkgseerService, config });
|
|
4139
|
+
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
|
|
3248
4140
|
}
|
|
3249
|
-
return
|
|
3250
|
-
`);
|
|
4141
|
+
return server;
|
|
3251
4142
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
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);
|
|
4143
|
+
function requireAuth(deps) {
|
|
4144
|
+
if (deps.hasValidToken) {
|
|
3259
4145
|
return;
|
|
3260
4146
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
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));
|
|
4147
|
+
console.log(`Authentication required to start MCP server.
|
|
4148
|
+
`);
|
|
4149
|
+
if (deps.baseUrl !== "https://pkgseer.dev") {
|
|
4150
|
+
console.log(` Environment: ${deps.baseUrl}`);
|
|
4151
|
+
console.log(` You're using a custom environment.
|
|
4152
|
+
`);
|
|
3277
4153
|
}
|
|
4154
|
+
console.log("To authenticate:");
|
|
4155
|
+
console.log(` pkgseer login
|
|
4156
|
+
`);
|
|
4157
|
+
console.log("Or set PKGSEER_API_TOKEN environment variable.");
|
|
4158
|
+
process.exit(1);
|
|
3278
4159
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
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
|
-
});
|
|
4160
|
+
async function startMcpServer(deps) {
|
|
4161
|
+
requireAuth(deps);
|
|
4162
|
+
const server = createMcpServer(deps);
|
|
4163
|
+
const transport = new StdioServerTransport;
|
|
4164
|
+
await server.connect(transport);
|
|
3298
4165
|
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
isDirectory,
|
|
3315
|
-
isRootOnly
|
|
3316
|
-
};
|
|
4166
|
+
function showMcpSetupInstructions(deps) {
|
|
4167
|
+
const useColors = shouldUseColors2();
|
|
4168
|
+
console.log("MCP Server Setup");
|
|
4169
|
+
console.log(`────────────────
|
|
4170
|
+
`);
|
|
4171
|
+
console.log(`Add PkgSeer to your AI assistant's MCP configuration.
|
|
4172
|
+
`);
|
|
4173
|
+
console.log(`${highlight("pkgseer mcp init", useColors)}`);
|
|
4174
|
+
console.log(dim(` Interactive setup for Cursor, Codex, or Claude Code
|
|
4175
|
+
`, useColors));
|
|
4176
|
+
console.log("Manual configuration:");
|
|
4177
|
+
console.log(` ${highlight(`${deps.baseUrl}/docs/mcp-server`, useColors)}
|
|
4178
|
+
`);
|
|
4179
|
+
console.log("Alternative: Use CLI directly (no MCP setup needed)");
|
|
4180
|
+
console.log(` ${highlight("pkgseer quickstart", useColors)}`);
|
|
3317
4181
|
}
|
|
3318
|
-
function
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
4182
|
+
function registerMcpCommand(program) {
|
|
4183
|
+
const mcpCommand = program.command("mcp").summary("Show setup instructions or start MCP server").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
|
|
4184
|
+
|
|
4185
|
+
When run interactively (TTY), shows setup instructions.
|
|
4186
|
+
When run via stdio (non-TTY), starts the MCP server.
|
|
4187
|
+
|
|
4188
|
+
Available tools: package_summary, package_vulnerabilities,
|
|
4189
|
+
package_dependencies, package_quality, compare_packages,
|
|
4190
|
+
list_package_docs, fetch_package_doc, search_package_docs,
|
|
4191
|
+
search_project_docs`).action(async () => {
|
|
4192
|
+
const deps = await createContainer();
|
|
4193
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
4194
|
+
showMcpSetupInstructions(deps);
|
|
4195
|
+
return;
|
|
3332
4196
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
4197
|
+
await startMcpServer(deps);
|
|
4198
|
+
});
|
|
4199
|
+
mcpCommand.command("start").summary("Start MCP server (stdio mode)").description(`Start the MCP server using STDIO transport.
|
|
4200
|
+
|
|
4201
|
+
This command explicitly starts the server and is intended for use
|
|
4202
|
+
in MCP configuration files. Use 'pkgseer mcp' for interactive setup.`).action(async () => {
|
|
4203
|
+
const deps = await createContainer();
|
|
4204
|
+
await startMcpServer(deps);
|
|
4205
|
+
});
|
|
4206
|
+
registerMcpInitCommand(mcpCommand);
|
|
3336
4207
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
4208
|
+
|
|
4209
|
+
// src/commands/pkg/compare.ts
|
|
4210
|
+
function parsePackageSpec(spec) {
|
|
4211
|
+
let registry = "npm";
|
|
4212
|
+
let rest = spec;
|
|
4213
|
+
if (spec.includes(":")) {
|
|
4214
|
+
const colonIndex = spec.indexOf(":");
|
|
4215
|
+
const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
|
|
4216
|
+
if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
|
|
4217
|
+
registry = potentialRegistry;
|
|
4218
|
+
rest = spec.slice(colonIndex + 1);
|
|
3348
4219
|
}
|
|
3349
|
-
return patterns.length > 0 ? patterns : null;
|
|
3350
|
-
} catch {
|
|
3351
|
-
return null;
|
|
3352
4220
|
}
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
ignored = false;
|
|
3361
|
-
} else {
|
|
3362
|
-
ignored = true;
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
4221
|
+
const atIndex = rest.lastIndexOf("@");
|
|
4222
|
+
if (atIndex > 0) {
|
|
4223
|
+
return {
|
|
4224
|
+
registry,
|
|
4225
|
+
name: rest.slice(0, atIndex),
|
|
4226
|
+
version: rest.slice(atIndex + 1)
|
|
4227
|
+
};
|
|
3365
4228
|
}
|
|
3366
|
-
return
|
|
3367
|
-
}
|
|
3368
|
-
function shouldIgnoreDirectory(relativeDirPath, patterns) {
|
|
3369
|
-
const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
|
|
3370
|
-
return shouldIgnorePath(normalized, patterns);
|
|
4229
|
+
return { registry, name: rest };
|
|
3371
4230
|
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
"
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
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";
|
|
4231
|
+
function formatPackageComparison(comparison) {
|
|
4232
|
+
const lines = [];
|
|
4233
|
+
lines.push("\uD83D\uDCCA Package Comparison");
|
|
4234
|
+
lines.push("");
|
|
4235
|
+
if (!comparison.packages || comparison.packages.length === 0) {
|
|
4236
|
+
lines.push("No packages to compare.");
|
|
4237
|
+
return lines.join(`
|
|
4238
|
+
`);
|
|
3409
4239
|
}
|
|
3410
|
-
|
|
4240
|
+
const packages = comparison.packages.filter((p) => p != null);
|
|
4241
|
+
const maxNameLen = Math.max(...packages.map((p) => `${p.packageName}@${p.version}`.length));
|
|
4242
|
+
const header = "Package".padEnd(maxNameLen + 2);
|
|
4243
|
+
lines.push(` ${header} Quality Downloads/mo Vulns`);
|
|
4244
|
+
lines.push(` ${"─".repeat(maxNameLen + 2)} ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(5)}`);
|
|
4245
|
+
for (const pkg of packages) {
|
|
4246
|
+
const name = `${pkg.packageName}@${pkg.version}`.padEnd(maxNameLen + 2);
|
|
4247
|
+
const scoreValue = pkg.quality?.score;
|
|
4248
|
+
const quality = scoreValue != null ? `${Math.round(scoreValue > 1 ? scoreValue : scoreValue * 100)}%`.padEnd(10) : "N/A".padEnd(10);
|
|
4249
|
+
const downloads = pkg.downloadsLastMonth ? formatNumber(pkg.downloadsLastMonth).padEnd(12) : "N/A".padEnd(12);
|
|
4250
|
+
const vulns = pkg.vulnerabilityCount != null ? pkg.vulnerabilityCount === 0 ? "✅ 0" : `⚠️ ${pkg.vulnerabilityCount}` : "N/A";
|
|
4251
|
+
lines.push(` ${name} ${quality} ${downloads} ${vulns}`);
|
|
4252
|
+
}
|
|
4253
|
+
return lines.join(`
|
|
4254
|
+
`);
|
|
3411
4255
|
}
|
|
3412
|
-
async function
|
|
3413
|
-
const
|
|
3414
|
-
if (
|
|
3415
|
-
|
|
4256
|
+
async function pkgCompareAction(packages, options, deps) {
|
|
4257
|
+
const { pkgseerService } = deps;
|
|
4258
|
+
if (packages.length < 2) {
|
|
4259
|
+
outputError("At least 2 packages required for comparison", options.json ?? false);
|
|
4260
|
+
return;
|
|
3416
4261
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
return detected;
|
|
4262
|
+
if (packages.length > 10) {
|
|
4263
|
+
outputError("Maximum 10 packages can be compared at once", options.json ?? false);
|
|
4264
|
+
return;
|
|
3421
4265
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
4266
|
+
const input2 = packages.map((spec) => {
|
|
4267
|
+
const parsed = parsePackageSpec(spec);
|
|
4268
|
+
return {
|
|
4269
|
+
registry: toGraphQLRegistry(parsed.registry),
|
|
4270
|
+
name: parsed.name,
|
|
4271
|
+
version: parsed.version
|
|
4272
|
+
};
|
|
4273
|
+
});
|
|
4274
|
+
const result = await pkgseerService.cliComparePackages(input2);
|
|
4275
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4276
|
+
if (!result.data.comparePackages) {
|
|
4277
|
+
outputError("Comparison failed", options.json ?? false);
|
|
4278
|
+
return;
|
|
3424
4279
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
relativePath,
|
|
3437
|
-
absolutePath: filePath,
|
|
3438
|
-
type: manifestType.type,
|
|
3439
|
-
suggestedLabel: suggestLabel(relativePath)
|
|
3440
|
-
});
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
4280
|
+
if (options.json) {
|
|
4281
|
+
const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
|
|
4282
|
+
const slim = pkgs.map((p) => ({
|
|
4283
|
+
package: `${p.packageName}@${p.version}`,
|
|
4284
|
+
quality: p.quality?.score,
|
|
4285
|
+
downloads: p.downloadsLastMonth,
|
|
4286
|
+
vulnerabilities: p.vulnerabilityCount
|
|
4287
|
+
}));
|
|
4288
|
+
output(slim, true);
|
|
4289
|
+
} else {
|
|
4290
|
+
console.log(formatPackageComparison(result.data.comparePackages));
|
|
3443
4291
|
}
|
|
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
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
} catch {}
|
|
3458
|
-
return detected;
|
|
3459
4292
|
}
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
4293
|
+
var COMPARE_DESCRIPTION = `Compare multiple packages.
|
|
4294
|
+
|
|
4295
|
+
Compares packages across quality, security, and popularity metrics.
|
|
4296
|
+
Supports cross-registry comparison.
|
|
4297
|
+
|
|
4298
|
+
Package format: [registry:]name[@version]
|
|
4299
|
+
- lodash (npm, latest)
|
|
4300
|
+
- pypi:requests (pypi, latest)
|
|
4301
|
+
- npm:express@4.18.0 (npm, specific version)
|
|
4302
|
+
|
|
4303
|
+
Examples:
|
|
4304
|
+
pkgseer pkg compare lodash underscore ramda
|
|
4305
|
+
pkgseer pkg compare npm:axios pypi:requests
|
|
4306
|
+
pkgseer pkg compare express@4.18.0 express@5.0.0 --json`;
|
|
4307
|
+
function registerPkgCompareCommand(program) {
|
|
4308
|
+
program.command("compare <packages...>").summary("Compare multiple packages").description(COMPARE_DESCRIPTION).option("--json", "Output as JSON").action(async (packages, options) => {
|
|
4309
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4310
|
+
const deps = await createContainer();
|
|
4311
|
+
await pkgCompareAction(packages, options, deps);
|
|
4312
|
+
});
|
|
4313
|
+
});
|
|
3468
4314
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
4315
|
+
// src/commands/pkg/deps.ts
|
|
4316
|
+
function formatPackageDependencies(data) {
|
|
4317
|
+
const lines = [];
|
|
4318
|
+
const pkg = data.package;
|
|
4319
|
+
const deps = data.dependencies;
|
|
4320
|
+
if (!pkg) {
|
|
4321
|
+
return "No package data available.";
|
|
4322
|
+
}
|
|
4323
|
+
lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.version}`);
|
|
4324
|
+
lines.push("");
|
|
4325
|
+
if (deps?.summary) {
|
|
4326
|
+
lines.push("Summary:");
|
|
4327
|
+
lines.push(` Direct dependencies: ${deps.summary.directCount ?? 0}`);
|
|
4328
|
+
if (deps.summary.uniquePackagesCount) {
|
|
4329
|
+
lines.push(` Unique packages: ${deps.summary.uniquePackagesCount}`);
|
|
3475
4330
|
}
|
|
3476
|
-
|
|
4331
|
+
lines.push("");
|
|
3477
4332
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
const
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
continue;
|
|
4333
|
+
if (deps?.direct && deps.direct.length > 0) {
|
|
4334
|
+
lines.push(`Dependencies (${deps.direct.length}):`);
|
|
4335
|
+
for (const dep of deps.direct) {
|
|
4336
|
+
if (dep) {
|
|
4337
|
+
const type = dep.type !== "RUNTIME" ? ` [${dep.type?.toLowerCase()}]` : "";
|
|
4338
|
+
lines.push(` ${dep.name} ${dep.versionConstraint}${type}`);
|
|
3485
4339
|
}
|
|
3486
|
-
filtered.push(manifest);
|
|
3487
4340
|
}
|
|
4341
|
+
} else {
|
|
4342
|
+
lines.push("No direct dependencies.");
|
|
3488
4343
|
}
|
|
3489
|
-
return
|
|
4344
|
+
return lines.join(`
|
|
4345
|
+
`);
|
|
3490
4346
|
}
|
|
3491
|
-
async function
|
|
3492
|
-
const
|
|
3493
|
-
const
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
4347
|
+
async function pkgDepsAction(packageName, options, deps) {
|
|
4348
|
+
const { pkgseerService } = deps;
|
|
4349
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4350
|
+
const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive);
|
|
4351
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4352
|
+
if (!result.data.packageDependencies) {
|
|
4353
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4354
|
+
return;
|
|
3499
4355
|
}
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
const
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
4356
|
+
if (options.json) {
|
|
4357
|
+
const data = result.data.packageDependencies;
|
|
4358
|
+
const slim = {
|
|
4359
|
+
package: `${data.package?.name}@${data.package?.version}`,
|
|
4360
|
+
directCount: data.dependencies?.summary?.directCount ?? 0,
|
|
4361
|
+
dependencies: data.dependencies?.direct?.filter((d) => d).map((d) => ({
|
|
4362
|
+
name: d.name,
|
|
4363
|
+
version: d.versionConstraint,
|
|
4364
|
+
type: d.type
|
|
4365
|
+
}))
|
|
4366
|
+
};
|
|
4367
|
+
output(slim, true);
|
|
4368
|
+
} else {
|
|
4369
|
+
console.log(formatPackageDependencies(result.data.packageDependencies));
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
var DEPS_DESCRIPTION = `Get package dependencies.
|
|
4373
|
+
|
|
4374
|
+
Lists direct dependencies and shows version constraints and
|
|
4375
|
+
dependency types (runtime, dev, optional).
|
|
4376
|
+
|
|
4377
|
+
Examples:
|
|
4378
|
+
pkgseer pkg deps express
|
|
4379
|
+
pkgseer pkg deps lodash --transitive
|
|
4380
|
+
pkgseer pkg deps requests --registry pypi --json`;
|
|
4381
|
+
function registerPkgDepsCommand(program) {
|
|
4382
|
+
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) => {
|
|
4383
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4384
|
+
const deps = await createContainer();
|
|
4385
|
+
await pkgDepsAction(packageName, options, deps);
|
|
4386
|
+
});
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
// src/commands/pkg/info.ts
|
|
4390
|
+
function formatPackageSummary(data) {
|
|
4391
|
+
const lines = [];
|
|
4392
|
+
const pkg = data.package;
|
|
4393
|
+
if (!pkg) {
|
|
4394
|
+
return "No package data available.";
|
|
4395
|
+
}
|
|
4396
|
+
lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.latestVersion}`);
|
|
4397
|
+
lines.push("");
|
|
4398
|
+
if (pkg.description) {
|
|
4399
|
+
lines.push(pkg.description);
|
|
4400
|
+
lines.push("");
|
|
4401
|
+
}
|
|
4402
|
+
lines.push("Package Info:");
|
|
4403
|
+
lines.push(keyValueTable([
|
|
4404
|
+
["Registry", pkg.registry],
|
|
4405
|
+
["Version", pkg.latestVersion],
|
|
4406
|
+
["License", pkg.license]
|
|
4407
|
+
]));
|
|
4408
|
+
lines.push("");
|
|
4409
|
+
if (pkg.homepage || pkg.repositoryUrl) {
|
|
4410
|
+
lines.push("Links:");
|
|
4411
|
+
const links = [];
|
|
4412
|
+
if (pkg.homepage)
|
|
4413
|
+
links.push(["Homepage", pkg.homepage]);
|
|
4414
|
+
if (pkg.repositoryUrl)
|
|
4415
|
+
links.push(["Repository", pkg.repositoryUrl]);
|
|
4416
|
+
lines.push(keyValueTable(links));
|
|
4417
|
+
lines.push("");
|
|
3520
4418
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
});
|
|
3533
|
-
return groups;
|
|
4419
|
+
const vulnCount = data.security?.vulnerabilityCount ?? 0;
|
|
4420
|
+
if (vulnCount > 0) {
|
|
4421
|
+
lines.push(`⚠️ Security: ${vulnCount} vulnerabilities`);
|
|
4422
|
+
lines.push("");
|
|
4423
|
+
}
|
|
4424
|
+
if (data.quickstart?.installCommand) {
|
|
4425
|
+
lines.push("Quick Start:");
|
|
4426
|
+
lines.push(` ${data.quickstart.installCommand}`);
|
|
4427
|
+
}
|
|
4428
|
+
return lines.join(`
|
|
4429
|
+
`);
|
|
3534
4430
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
allow_mix_deps: group.allow_mix_deps
|
|
3544
|
-
});
|
|
3545
|
-
}
|
|
4431
|
+
async function pkgInfoAction(packageName, options, deps) {
|
|
4432
|
+
const { pkgseerService } = deps;
|
|
4433
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4434
|
+
const result = await pkgseerService.cliPackageInfo(registry, packageName);
|
|
4435
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4436
|
+
if (!result.data.packageSummary) {
|
|
4437
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
4438
|
+
return;
|
|
3546
4439
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
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
|
-
}
|
|
4440
|
+
if (options.json) {
|
|
4441
|
+
const data = result.data.packageSummary;
|
|
4442
|
+
const pkg = data.package;
|
|
4443
|
+
const slim = {
|
|
4444
|
+
name: pkg?.name,
|
|
4445
|
+
version: pkg?.latestVersion,
|
|
4446
|
+
description: pkg?.description,
|
|
4447
|
+
license: pkg?.license,
|
|
4448
|
+
install: data.quickstart?.installCommand,
|
|
4449
|
+
vulnerabilities: data.security?.vulnerabilityCount ?? 0
|
|
4450
|
+
};
|
|
4451
|
+
output(slim, true);
|
|
4452
|
+
} else {
|
|
4453
|
+
console.log(formatPackageSummary(result.data.packageSummary));
|
|
3572
4454
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
4455
|
+
}
|
|
4456
|
+
var INFO_DESCRIPTION = `Get package summary and metadata.
|
|
4457
|
+
|
|
4458
|
+
Displays comprehensive information about a package including:
|
|
4459
|
+
- Basic metadata (version, license, description)
|
|
4460
|
+
- Download statistics
|
|
4461
|
+
- Security advisories
|
|
4462
|
+
- Quick start instructions
|
|
4463
|
+
|
|
4464
|
+
Examples:
|
|
4465
|
+
pkgseer pkg info lodash
|
|
4466
|
+
pkgseer pkg info requests --registry pypi
|
|
4467
|
+
pkgseer pkg info phoenix --registry hex --json`;
|
|
4468
|
+
function registerPkgInfoCommand(program) {
|
|
4469
|
+
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) => {
|
|
4470
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4471
|
+
const deps = await createContainer();
|
|
4472
|
+
await pkgInfoAction(packageName, options, deps);
|
|
3587
4473
|
});
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
// src/commands/pkg/quality.ts
|
|
4477
|
+
function formatPackageQuality(data) {
|
|
4478
|
+
const lines = [];
|
|
4479
|
+
const quality = data.quality;
|
|
4480
|
+
if (!quality) {
|
|
4481
|
+
return "No quality data available.";
|
|
3588
4482
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4483
|
+
lines.push(`\uD83D\uDCCA Quality Score: ${formatScore(quality.overallScore)} (Grade: ${quality.grade})`);
|
|
4484
|
+
lines.push("");
|
|
4485
|
+
if (quality.categories && quality.categories.length > 0) {
|
|
4486
|
+
lines.push("Category Breakdown:");
|
|
4487
|
+
for (const category of quality.categories) {
|
|
4488
|
+
if (category) {
|
|
4489
|
+
const name = (category.category || "Unknown").padEnd(20);
|
|
4490
|
+
lines.push(` ${name} ${formatScore(category.score)}`);
|
|
3593
4491
|
}
|
|
3594
|
-
return -1;
|
|
3595
4492
|
}
|
|
3596
|
-
|
|
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);
|
|
4493
|
+
lines.push("");
|
|
3611
4494
|
}
|
|
3612
|
-
|
|
3613
|
-
const existingManifests = projectConfig.config.manifests ?? [];
|
|
3614
|
-
console.log(`Scanning for manifest files in project "${projectName}"...
|
|
4495
|
+
return lines.join(`
|
|
3615
4496
|
`);
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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
|
-
}
|
|
4497
|
+
}
|
|
4498
|
+
async function pkgQualityAction(packageName, options, deps) {
|
|
4499
|
+
const { pkgseerService } = deps;
|
|
4500
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4501
|
+
const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
|
|
4502
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4503
|
+
if (!result.data.packageQuality) {
|
|
4504
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
3630
4505
|
return;
|
|
3631
4506
|
}
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
4507
|
+
if (options.json) {
|
|
4508
|
+
const quality = result.data.packageQuality.quality;
|
|
4509
|
+
const slim = {
|
|
4510
|
+
package: `${result.data.packageQuality.package?.name}@${result.data.packageQuality.package?.version}`,
|
|
4511
|
+
score: quality?.overallScore,
|
|
4512
|
+
grade: quality?.grade,
|
|
4513
|
+
categories: quality?.categories?.filter((c) => c).map((c) => ({
|
|
4514
|
+
name: c.category,
|
|
4515
|
+
score: c.score
|
|
4516
|
+
}))
|
|
4517
|
+
};
|
|
4518
|
+
output(slim, true);
|
|
3636
4519
|
} else {
|
|
3637
|
-
|
|
3638
|
-
console.log(` Label: ${group.label}`);
|
|
3639
|
-
for (const file of group.files) {
|
|
3640
|
-
console.log(` ${file}`);
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
4520
|
+
console.log(formatPackageQuality(result.data.packageQuality));
|
|
3643
4521
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
4522
|
+
}
|
|
4523
|
+
var QUALITY_DESCRIPTION = `Get package quality score and breakdown.
|
|
4524
|
+
|
|
4525
|
+
Analyzes package quality across multiple dimensions:
|
|
4526
|
+
- Maintenance and activity
|
|
4527
|
+
- Documentation coverage
|
|
4528
|
+
- Security practices
|
|
4529
|
+
- Community engagement
|
|
4530
|
+
|
|
4531
|
+
Examples:
|
|
4532
|
+
pkgseer pkg quality lodash
|
|
4533
|
+
pkgseer pkg quality express -v 4.18.0
|
|
4534
|
+
pkgseer pkg quality requests --registry pypi --json`;
|
|
4535
|
+
function registerPkgQualityCommand(program) {
|
|
4536
|
+
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) => {
|
|
4537
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4538
|
+
const deps = await createContainer();
|
|
4539
|
+
await pkgQualityAction(packageName, options, deps);
|
|
4540
|
+
});
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
// src/commands/pkg/vulns.ts
|
|
4544
|
+
function getSeverityLabel(score) {
|
|
4545
|
+
if (score == null)
|
|
4546
|
+
return "UNKNOWN";
|
|
4547
|
+
if (score >= 9)
|
|
4548
|
+
return "CRITICAL";
|
|
4549
|
+
if (score >= 7)
|
|
4550
|
+
return "HIGH";
|
|
4551
|
+
if (score >= 4)
|
|
4552
|
+
return "MEDIUM";
|
|
4553
|
+
return "LOW";
|
|
4554
|
+
}
|
|
4555
|
+
function formatPackageVulnerabilities(data) {
|
|
4556
|
+
const lines = [];
|
|
4557
|
+
const pkg = data.package;
|
|
4558
|
+
const security = data.security;
|
|
4559
|
+
if (!pkg) {
|
|
4560
|
+
return "No package data available.";
|
|
4561
|
+
}
|
|
4562
|
+
lines.push(`\uD83D\uDD12 Security Report: ${pkg.name}@${pkg.version}`);
|
|
4563
|
+
lines.push("");
|
|
4564
|
+
if (!security?.vulnerabilities || security.vulnerabilities.length === 0) {
|
|
4565
|
+
lines.push("✅ No known vulnerabilities!");
|
|
4566
|
+
return lines.join(`
|
|
4567
|
+
`);
|
|
4568
|
+
}
|
|
4569
|
+
const bySeverity = security.vulnerabilities.reduce((acc, v) => {
|
|
4570
|
+
if (v) {
|
|
4571
|
+
const label = getSeverityLabel(v.severityScore);
|
|
4572
|
+
acc[label] = (acc[label] || 0) + 1;
|
|
3658
4573
|
}
|
|
4574
|
+
return acc;
|
|
4575
|
+
}, {});
|
|
4576
|
+
lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
|
|
4577
|
+
for (const [severity, count] of Object.entries(bySeverity)) {
|
|
4578
|
+
lines.push(` ${formatSeverity(severity)}: ${count}`);
|
|
3659
4579
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
} else {
|
|
3672
|
-
allowMixDeps = true;
|
|
4580
|
+
lines.push("");
|
|
4581
|
+
lines.push("Details:");
|
|
4582
|
+
for (const vuln of security.vulnerabilities) {
|
|
4583
|
+
if (!vuln)
|
|
4584
|
+
continue;
|
|
4585
|
+
lines.push("");
|
|
4586
|
+
lines.push(` ${formatSeverity(getSeverityLabel(vuln.severityScore))}`);
|
|
4587
|
+
lines.push(` ${vuln.summary || vuln.osvId}`);
|
|
4588
|
+
lines.push(` ID: ${vuln.osvId}`);
|
|
4589
|
+
if (vuln.fixedInVersions && vuln.fixedInVersions.length > 0) {
|
|
4590
|
+
lines.push(` Fixed in: ${vuln.fixedInVersions.join(", ")}`);
|
|
3673
4591
|
}
|
|
3674
4592
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
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.`);
|
|
4593
|
+
return lines.join(`
|
|
4594
|
+
`);
|
|
4595
|
+
}
|
|
4596
|
+
async function pkgVulnsAction(packageName, options, deps) {
|
|
4597
|
+
const { pkgseerService } = deps;
|
|
4598
|
+
const registry = toGraphQLRegistry(options.registry);
|
|
4599
|
+
const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
|
|
4600
|
+
handleErrors(result.errors, options.json ?? false);
|
|
4601
|
+
if (!result.data.packageVulnerabilities) {
|
|
4602
|
+
outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
|
|
3702
4603
|
return;
|
|
3703
4604
|
}
|
|
3704
|
-
if (options.
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
4605
|
+
if (options.json) {
|
|
4606
|
+
const data = result.data.packageVulnerabilities;
|
|
4607
|
+
const vulns = data.security?.vulnerabilities ?? [];
|
|
4608
|
+
const slim = {
|
|
4609
|
+
package: `${data.package?.name}@${data.package?.version}`,
|
|
4610
|
+
count: data.security?.vulnerabilityCount ?? 0,
|
|
4611
|
+
vulnerabilities: vulns.filter((v) => v).map((v) => ({
|
|
4612
|
+
id: v.osvId,
|
|
4613
|
+
severity: v.severityScore,
|
|
4614
|
+
summary: v.summary,
|
|
4615
|
+
fixed: v.fixedInVersions
|
|
3711
4616
|
}))
|
|
3712
|
-
}
|
|
3713
|
-
|
|
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.`);
|
|
4617
|
+
};
|
|
4618
|
+
output(slim, true);
|
|
3718
4619
|
} else {
|
|
3719
|
-
|
|
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
|
-
}))
|
|
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`);
|
|
3739
|
-
}
|
|
4620
|
+
console.log(formatPackageVulnerabilities(result.data.packageVulnerabilities));
|
|
3740
4621
|
}
|
|
3741
4622
|
}
|
|
3742
|
-
var
|
|
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:
|
|
4623
|
+
var VULNS_DESCRIPTION = `Check package for security vulnerabilities.
|
|
3747
4624
|
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4625
|
+
Scans for known security vulnerabilities and provides:
|
|
4626
|
+
- Severity ratings (critical, high, medium, low)
|
|
4627
|
+
- CVE identifiers
|
|
4628
|
+
- Affected version ranges
|
|
4629
|
+
- Upgrade recommendations
|
|
3751
4630
|
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
shellService: deps.shellService
|
|
4631
|
+
Examples:
|
|
4632
|
+
pkgseer pkg vulns lodash
|
|
4633
|
+
pkgseer pkg vulns express -v 4.17.0
|
|
4634
|
+
pkgseer pkg vulns requests --registry pypi --json`;
|
|
4635
|
+
function registerPkgVulnsCommand(program) {
|
|
4636
|
+
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) => {
|
|
4637
|
+
await withCliErrorHandling(options.json ?? false, async () => {
|
|
4638
|
+
const deps = await createContainer();
|
|
4639
|
+
await pkgVulnsAction(packageName, options, deps);
|
|
3762
4640
|
});
|
|
3763
4641
|
});
|
|
3764
4642
|
}
|
|
3765
|
-
// src/commands/
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
cyan: "\x1B[36m",
|
|
3775
|
-
red: "\x1B[31m"
|
|
3776
|
-
};
|
|
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");
|
|
4643
|
+
// src/commands/project/detect.ts
|
|
4644
|
+
function matchManifestsWithConfig(detectedGroups, existingManifests) {
|
|
4645
|
+
const fileToConfig = new Map;
|
|
4646
|
+
for (const group of existingManifests) {
|
|
4647
|
+
for (const file of group.files) {
|
|
4648
|
+
fileToConfig.set(file, {
|
|
4649
|
+
label: group.label,
|
|
4650
|
+
allow_mix_deps: group.allow_mix_deps
|
|
4651
|
+
});
|
|
3819
4652
|
}
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
const
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
4653
|
+
}
|
|
4654
|
+
const labelToFiles = new Map;
|
|
4655
|
+
const labelToConfig = new Map;
|
|
4656
|
+
for (const detectedGroup of detectedGroups) {
|
|
4657
|
+
for (const manifest of detectedGroup.manifests) {
|
|
4658
|
+
const existingConfig = fileToConfig.get(manifest.relativePath);
|
|
4659
|
+
let label;
|
|
4660
|
+
const hasEcosystemSuffix = detectedGroup.label.endsWith("-npm") || detectedGroup.label.endsWith("-hex") || detectedGroup.label.endsWith("-pypi");
|
|
4661
|
+
if (hasEcosystemSuffix) {
|
|
4662
|
+
label = detectedGroup.label;
|
|
4663
|
+
} else {
|
|
4664
|
+
label = existingConfig?.label ?? detectedGroup.label;
|
|
4665
|
+
}
|
|
4666
|
+
const allowMixDeps = existingConfig?.allow_mix_deps;
|
|
4667
|
+
if (!labelToConfig.has(label)) {
|
|
4668
|
+
labelToConfig.set(label, {
|
|
4669
|
+
files: new Set,
|
|
4670
|
+
allow_mix_deps: allowMixDeps
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
const config = labelToConfig.get(label);
|
|
4674
|
+
config.files.add(manifest.relativePath);
|
|
4675
|
+
if (allowMixDeps) {
|
|
4676
|
+
config.allow_mix_deps = true;
|
|
4677
|
+
}
|
|
3844
4678
|
}
|
|
3845
4679
|
}
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4680
|
+
const result = [];
|
|
4681
|
+
for (const [label, config] of labelToConfig.entries()) {
|
|
4682
|
+
const fileArray = Array.from(config.files);
|
|
4683
|
+
const hasNewFiles = fileArray.some((file) => !fileToConfig.has(file));
|
|
4684
|
+
const hasChangedLabels = fileArray.some((file) => {
|
|
4685
|
+
const oldConfig = fileToConfig.get(file);
|
|
4686
|
+
return oldConfig !== undefined && oldConfig.label !== label;
|
|
4687
|
+
});
|
|
4688
|
+
result.push({
|
|
4689
|
+
label,
|
|
4690
|
+
files: fileArray.sort(),
|
|
4691
|
+
isNew: hasNewFiles,
|
|
4692
|
+
changedLabel: hasChangedLabels ? label : undefined,
|
|
4693
|
+
allow_mix_deps: config.allow_mix_deps
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
return result.sort((a, b) => {
|
|
4697
|
+
if (a.label.startsWith("root")) {
|
|
4698
|
+
if (b.label.startsWith("root")) {
|
|
4699
|
+
return a.label.localeCompare(b.label);
|
|
3851
4700
|
}
|
|
3852
|
-
return
|
|
4701
|
+
return -1;
|
|
3853
4702
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
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.`);
|
|
4703
|
+
if (b.label.startsWith("root")) {
|
|
4704
|
+
return 1;
|
|
3859
4705
|
}
|
|
3860
|
-
|
|
3861
|
-
const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
|
|
3862
|
-
return {
|
|
3863
|
-
filename,
|
|
3864
|
-
path: absolutePath,
|
|
3865
|
-
content
|
|
3866
|
-
};
|
|
4706
|
+
return a.label.localeCompare(b.label);
|
|
3867
4707
|
});
|
|
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;
|
|
3890
4708
|
}
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
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`);
|
|
4709
|
+
async function projectDetectAction(options, deps) {
|
|
4710
|
+
const { configService, fileSystemService, promptService, shellService } = deps;
|
|
4711
|
+
const projectConfig = await configService.loadProjectConfig();
|
|
4712
|
+
if (!projectConfig?.config.project) {
|
|
4713
|
+
console.error(`✗ No project is configured in pkgseer.yml`);
|
|
3925
4714
|
console.log(`
|
|
3926
|
-
|
|
3927
|
-
|
|
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));
|
|
4715
|
+
To get started, run: pkgseer project init`);
|
|
4716
|
+
console.log(` This will create a project and detect your manifest files.`);
|
|
3935
4717
|
process.exit(1);
|
|
3936
4718
|
}
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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
|
-
};
|
|
4719
|
+
const projectName = projectConfig.config.project;
|
|
4720
|
+
const existingManifests = projectConfig.config.manifests ?? [];
|
|
4721
|
+
console.log(`Scanning for manifest files in project "${projectName}"...
|
|
4722
|
+
`);
|
|
4723
|
+
const cwd = fileSystemService.getCwd();
|
|
4724
|
+
const detectedGroups = await detectAndGroupManifests(cwd, fileSystemService, {
|
|
4725
|
+
maxDepth: options.maxDepth ?? 3
|
|
4726
|
+
});
|
|
4727
|
+
if (detectedGroups.length === 0) {
|
|
4728
|
+
console.log("No manifest files were found in the current directory.");
|
|
4729
|
+
if (existingManifests.length > 0) {
|
|
4730
|
+
console.log(`
|
|
4731
|
+
Your existing configuration in pkgseer.yml will remain unchanged.`);
|
|
4732
|
+
console.log(` Tip: Make sure you're running this command from the project root directory.`);
|
|
3973
4733
|
} else {
|
|
3974
|
-
console.
|
|
3975
|
-
|
|
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);
|
|
4734
|
+
console.log(`
|
|
4735
|
+
Tip: Manifest files like package.json, requirements.txt, or pyproject.toml should be in the current directory or subdirectories.`);
|
|
3992
4736
|
}
|
|
4737
|
+
return;
|
|
3993
4738
|
}
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4739
|
+
const suggestedManifests = matchManifestsWithConfig(detectedGroups, existingManifests);
|
|
4740
|
+
console.log("Current configuration in pkgseer.yml:");
|
|
4741
|
+
if (existingManifests.length === 0) {
|
|
4742
|
+
console.log(" (no manifests configured yet)");
|
|
4743
|
+
} else {
|
|
4744
|
+
for (const group of existingManifests) {
|
|
4745
|
+
console.log(` Label: ${group.label}`);
|
|
4746
|
+
for (const file of group.files) {
|
|
4747
|
+
console.log(` ${file}`);
|
|
4001
4748
|
}
|
|
4002
|
-
process.exit(1);
|
|
4003
4749
|
}
|
|
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
4750
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
};
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
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
|
-
}
|
|
4751
|
+
console.log(`
|
|
4752
|
+
Suggested configuration:`);
|
|
4753
|
+
for (const group of suggestedManifests) {
|
|
4754
|
+
const markers = [];
|
|
4755
|
+
if (group.isNew)
|
|
4756
|
+
markers.push("new");
|
|
4757
|
+
if (group.changedLabel)
|
|
4758
|
+
markers.push("label changed");
|
|
4759
|
+
const markerStr = markers.length > 0 ? ` (${markers.join(", ")})` : "";
|
|
4760
|
+
console.log(` Label: ${group.label}${markerStr}`);
|
|
4761
|
+
for (const file of group.files) {
|
|
4762
|
+
const wasInConfig = existingManifests.some((g) => g.files.includes(file));
|
|
4763
|
+
const prefix = wasInConfig ? " " : " + ";
|
|
4764
|
+
console.log(`${prefix}${file}`);
|
|
4033
4765
|
}
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4766
|
+
}
|
|
4767
|
+
const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
|
|
4768
|
+
const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
|
|
4769
|
+
let allowMixDeps = false;
|
|
4770
|
+
if (hasHexInSuggested) {
|
|
4771
|
+
const existingHasMixDeps = existingManifests.some((g) => g.allow_mix_deps === true);
|
|
4772
|
+
if (!existingHasMixDeps) {
|
|
4773
|
+
console.log(`
|
|
4774
|
+
Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`);
|
|
4775
|
+
console.log(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`);
|
|
4040
4776
|
allowMixDeps = await promptService.confirm(`
|
|
4041
4777
|
Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
|
|
4778
|
+
} else {
|
|
4779
|
+
allowMixDeps = true;
|
|
4042
4780
|
}
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4781
|
+
}
|
|
4782
|
+
const finalManifests = suggestedManifests.map((g) => {
|
|
4783
|
+
const hasHexInGroup = g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
|
|
4784
|
+
return {
|
|
4785
|
+
label: g.label,
|
|
4786
|
+
files: g.files,
|
|
4787
|
+
isNew: g.isNew,
|
|
4788
|
+
changedLabel: g.changedLabel,
|
|
4789
|
+
...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {},
|
|
4790
|
+
...g.allow_mix_deps !== undefined ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4791
|
+
};
|
|
4792
|
+
});
|
|
4793
|
+
const hasChanges = finalManifests.length !== existingManifests.length || finalManifests.some((g) => g.isNew || g.changedLabel) || existingManifests.some((existing) => {
|
|
4794
|
+
const suggested = finalManifests.find((s) => s.label === existing.label);
|
|
4795
|
+
if (!suggested)
|
|
4796
|
+
return true;
|
|
4797
|
+
const existingFiles = new Set(existing.files);
|
|
4798
|
+
const suggestedFiles = new Set(suggested.files);
|
|
4799
|
+
return existingFiles.size !== suggestedFiles.size || !Array.from(existingFiles).every((f) => suggestedFiles.has(f));
|
|
4800
|
+
}) || finalManifests.some((g) => {
|
|
4801
|
+
const existing = existingManifests.find((e) => e.label === g.label);
|
|
4802
|
+
return existing?.allow_mix_deps !== g.allow_mix_deps;
|
|
4803
|
+
});
|
|
4804
|
+
if (!hasChanges) {
|
|
4805
|
+
console.log(`
|
|
4806
|
+
✓ Your configuration is already up to date!`);
|
|
4807
|
+
console.log(`
|
|
4808
|
+
All detected manifest files match your current pkgseer.yml configuration.`);
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
if (options.update) {
|
|
4812
|
+
await configService.writeProjectConfig({
|
|
4813
|
+
project: projectName,
|
|
4814
|
+
manifests: finalManifests.map((g) => ({
|
|
4815
|
+
label: g.label,
|
|
4816
|
+
files: g.files,
|
|
4817
|
+
...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4818
|
+
}))
|
|
4050
4819
|
});
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
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;
|
|
4820
|
+
console.log(`
|
|
4821
|
+
✓ Configuration updated successfully!`);
|
|
4822
|
+
console.log(`
|
|
4823
|
+
Your pkgseer.yml has been updated with the detected manifest files.`);
|
|
4824
|
+
console.log(` Run 'pkgseer project upload' to upload them to your project.`);
|
|
4825
|
+
} else {
|
|
4826
|
+
const shouldUpdate = await promptService.confirm(`
|
|
4827
|
+
Would you like to update pkgseer.yml with these changes?`, false);
|
|
4828
|
+
if (shouldUpdate) {
|
|
4829
|
+
await configService.writeProjectConfig({
|
|
4830
|
+
project: projectName,
|
|
4831
|
+
manifests: finalManifests.map((g) => ({
|
|
4832
|
+
label: g.label,
|
|
4833
|
+
files: g.files,
|
|
4834
|
+
...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
|
|
4835
|
+
}))
|
|
4836
|
+
});
|
|
4088
4837
|
console.log(`
|
|
4089
|
-
|
|
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));
|
|
4146
|
-
}
|
|
4147
|
-
}
|
|
4838
|
+
✓ Configuration updated successfully!`);
|
|
4148
4839
|
console.log(`
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
To
|
|
4154
|
-
|
|
4840
|
+
Your pkgseer.yml has been updated. Run 'pkgseer project upload' to upload the manifests.`);
|
|
4841
|
+
} else {
|
|
4842
|
+
console.log(`
|
|
4843
|
+
Configuration was not updated.`);
|
|
4844
|
+
console.log(` To apply these changes automatically, run: pkgseer project detect --update`);
|
|
4845
|
+
console.log(` Or manually edit pkgseer.yml and then run: pkgseer project upload`);
|
|
4155
4846
|
}
|
|
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
4847
|
}
|
|
4184
4848
|
}
|
|
4185
|
-
var
|
|
4849
|
+
var DETECT_DESCRIPTION = `Detect manifest files and update your project configuration.
|
|
4186
4850
|
|
|
4187
|
-
This command
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
3. Suggest labels based on directory structure
|
|
4191
|
-
4. Optionally upload manifests to the project
|
|
4851
|
+
This command scans your project directory for manifest files (like
|
|
4852
|
+
package.json, requirements.txt, etc.) and compares them with your
|
|
4853
|
+
current pkgseer.yml configuration. It will:
|
|
4192
4854
|
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4855
|
+
• Preserve existing labels for files you've already configured
|
|
4856
|
+
• Suggest new labels for newly detected files
|
|
4857
|
+
• Show you what would change before updating
|
|
4858
|
+
|
|
4859
|
+
Perfect for when you add new manifest files or reorganize your
|
|
4860
|
+
project structure. Run with --update to automatically apply changes.`;
|
|
4861
|
+
function registerProjectDetectCommand(program) {
|
|
4862
|
+
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) => {
|
|
4198
4863
|
const deps = await createContainer();
|
|
4199
|
-
await
|
|
4200
|
-
projectService: deps.projectService,
|
|
4864
|
+
await projectDetectAction({ maxDepth: options.maxDepth, update: options.update }, {
|
|
4201
4865
|
configService: deps.configService,
|
|
4202
4866
|
fileSystemService: deps.fileSystemService,
|
|
4203
|
-
gitService: deps.gitService,
|
|
4204
4867
|
promptService: deps.promptService,
|
|
4205
|
-
shellService: deps.shellService
|
|
4206
|
-
authStorage: deps.authStorage,
|
|
4207
|
-
baseUrl: deps.baseUrl
|
|
4868
|
+
shellService: deps.shellService
|
|
4208
4869
|
});
|
|
4209
4870
|
});
|
|
4210
4871
|
}
|
|
@@ -4217,11 +4878,10 @@ async function projectUploadAction(options, deps) {
|
|
|
4217
4878
|
gitService,
|
|
4218
4879
|
shellService,
|
|
4219
4880
|
promptService,
|
|
4220
|
-
authStorage,
|
|
4221
4881
|
baseUrl
|
|
4222
4882
|
} = deps;
|
|
4223
4883
|
const useColors = shouldUseColors2();
|
|
4224
|
-
const tokenData = await checkProjectWriteScope(
|
|
4884
|
+
const tokenData = await checkProjectWriteScope(configService, baseUrl);
|
|
4225
4885
|
if (!tokenData) {
|
|
4226
4886
|
console.error(error(`Authentication required. Please run 'pkgseer login'.`, useColors));
|
|
4227
4887
|
console.log(`
|
|
@@ -4232,17 +4892,6 @@ async function projectUploadAction(options, deps) {
|
|
|
4232
4892
|
Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
|
|
4233
4893
|
process.exit(1);
|
|
4234
4894
|
}
|
|
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);
|
|
4245
|
-
}
|
|
4246
4895
|
const projectConfig = await configService.loadProjectConfig();
|
|
4247
4896
|
if (!projectConfig?.config.project) {
|
|
4248
4897
|
console.error(error(`No project is configured in pkgseer.yml`, useColors));
|
|
@@ -4411,7 +5060,6 @@ function registerProjectUploadCommand(program) {
|
|
|
4411
5060
|
gitService: deps.gitService,
|
|
4412
5061
|
shellService: deps.shellService,
|
|
4413
5062
|
promptService: deps.promptService,
|
|
4414
|
-
authStorage: deps.authStorage,
|
|
4415
5063
|
baseUrl: deps.baseUrl
|
|
4416
5064
|
});
|
|
4417
5065
|
});
|
|
@@ -4510,9 +5158,9 @@ function registerQuickstartCommand(program) {
|
|
|
4510
5158
|
var program = new Command;
|
|
4511
5159
|
program.name("pkgseer").description("Package intelligence for your AI assistant").version(version).addHelpText("after", `
|
|
4512
5160
|
Getting started:
|
|
4513
|
-
pkgseer
|
|
4514
|
-
pkgseer
|
|
4515
|
-
pkgseer quickstart
|
|
5161
|
+
pkgseer init Interactive setup wizard (project + MCP)
|
|
5162
|
+
pkgseer login Authenticate with your account
|
|
5163
|
+
pkgseer quickstart Quick reference for AI agents
|
|
4516
5164
|
|
|
4517
5165
|
Package commands:
|
|
4518
5166
|
pkgseer pkg info <package> Get package summary
|
|
@@ -4527,6 +5175,7 @@ Documentation commands:
|
|
|
4527
5175
|
pkgseer docs search <query> Search documentation
|
|
4528
5176
|
|
|
4529
5177
|
Learn more at https://pkgseer.dev`);
|
|
5178
|
+
registerInitCommand(program);
|
|
4530
5179
|
registerMcpCommand(program);
|
|
4531
5180
|
registerLoginCommand(program);
|
|
4532
5181
|
registerLogoutCommand(program);
|