lurqrun 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +105 -0
- package/dist/bin/lurq.js +2848 -0
- package/dist/bin/lurq.js.map +1 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +2860 -0
- package/dist/index.js.map +1 -0
- package/drizzle/0000_confused_fabian_cortez.sql +54 -0
- package/drizzle/meta/0000_snapshot.json +370 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +76 -0
- package/src/data/seed.json +253 -0
- package/templates/skill-instructions.md +32 -0
package/dist/bin/lurq.js
ADDED
|
@@ -0,0 +1,2848 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/core/constants.ts
|
|
22
|
+
var SERVER_NAME, PACKAGE_NAME, VERSION, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
|
|
23
|
+
var init_constants = __esm({
|
|
24
|
+
"src/core/constants.ts"() {
|
|
25
|
+
"use strict";
|
|
26
|
+
init_esm_shims();
|
|
27
|
+
SERVER_NAME = "lurq";
|
|
28
|
+
PACKAGE_NAME = "lurqrun";
|
|
29
|
+
VERSION = "0.0.1";
|
|
30
|
+
EMBEDDING_DIM = 1536;
|
|
31
|
+
STALENESS_DAYS = 7;
|
|
32
|
+
HOUR = 60 * 60 * 1e3;
|
|
33
|
+
DAY = 24 * HOUR;
|
|
34
|
+
CACHE_TTL = {
|
|
35
|
+
npmRegistry: 6 * HOUR,
|
|
36
|
+
npmDownloads: 12 * HOUR,
|
|
37
|
+
github: 12 * HOUR,
|
|
38
|
+
depsDev: 24 * HOUR,
|
|
39
|
+
bundlephobia: 7 * DAY
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// src/core/types.ts
|
|
45
|
+
function isCategory(value) {
|
|
46
|
+
return CATEGORIES.includes(value);
|
|
47
|
+
}
|
|
48
|
+
function isFrontendCategory(category) {
|
|
49
|
+
return category != null && FRONTEND_CATEGORIES.includes(category);
|
|
50
|
+
}
|
|
51
|
+
var CATEGORIES, FRONTEND_CATEGORIES;
|
|
52
|
+
var init_types = __esm({
|
|
53
|
+
"src/core/types.ts"() {
|
|
54
|
+
"use strict";
|
|
55
|
+
init_esm_shims();
|
|
56
|
+
CATEGORIES = [
|
|
57
|
+
"framework",
|
|
58
|
+
"meta-framework",
|
|
59
|
+
"state-management",
|
|
60
|
+
"routing",
|
|
61
|
+
"orm",
|
|
62
|
+
"database-client",
|
|
63
|
+
"ui-component-library",
|
|
64
|
+
"styling",
|
|
65
|
+
"forms",
|
|
66
|
+
"validation",
|
|
67
|
+
"data-fetching",
|
|
68
|
+
"http-client",
|
|
69
|
+
"auth",
|
|
70
|
+
"testing",
|
|
71
|
+
"build-tool",
|
|
72
|
+
"bundler",
|
|
73
|
+
"linting",
|
|
74
|
+
"date-time",
|
|
75
|
+
"animation",
|
|
76
|
+
"charts",
|
|
77
|
+
"i18n",
|
|
78
|
+
"utility",
|
|
79
|
+
"other"
|
|
80
|
+
];
|
|
81
|
+
FRONTEND_CATEGORIES = [
|
|
82
|
+
"ui-component-library",
|
|
83
|
+
"styling",
|
|
84
|
+
"state-management",
|
|
85
|
+
"forms",
|
|
86
|
+
"validation",
|
|
87
|
+
"data-fetching",
|
|
88
|
+
"charts",
|
|
89
|
+
"animation",
|
|
90
|
+
"date-time",
|
|
91
|
+
"utility"
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/core/config.ts
|
|
97
|
+
import { config as dotenvConfig } from "dotenv";
|
|
98
|
+
import { z } from "zod";
|
|
99
|
+
function loadEnv() {
|
|
100
|
+
if (envLoaded) return;
|
|
101
|
+
dotenvConfig();
|
|
102
|
+
envLoaded = true;
|
|
103
|
+
}
|
|
104
|
+
function getConfig() {
|
|
105
|
+
if (cached) return cached;
|
|
106
|
+
loadEnv();
|
|
107
|
+
const parsed = EnvSchema.safeParse(process.env);
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
110
|
+
throw new ConfigError(`Invalid environment configuration:
|
|
111
|
+
${issues}`);
|
|
112
|
+
}
|
|
113
|
+
cached = parsed.data;
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
function requireConfig(keys) {
|
|
117
|
+
const config = getConfig();
|
|
118
|
+
const missing = keys.filter((k) => config[k] === void 0 || config[k] === "");
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
throw new ConfigError(
|
|
121
|
+
`Missing required environment variable(s): ${missing.join(", ")}.
|
|
122
|
+
See .env.example for setup. Tip: copy it to .env and fill in the values.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
var envLoaded, EnvSchema, ConfigError, cached;
|
|
128
|
+
var init_config = __esm({
|
|
129
|
+
"src/core/config.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
init_esm_shims();
|
|
132
|
+
envLoaded = false;
|
|
133
|
+
EnvSchema = z.object({
|
|
134
|
+
DATABASE_URL: z.string().min(1).optional(),
|
|
135
|
+
GITHUB_TOKEN: z.string().min(1).optional(),
|
|
136
|
+
EMBEDDING_PROVIDER: z.enum(["openai", "local"]).default("openai"),
|
|
137
|
+
EMBEDDING_API_KEY: z.string().min(1).optional(),
|
|
138
|
+
EMBEDDING_MODEL: z.string().min(1).default("text-embedding-3-small"),
|
|
139
|
+
SUMMARY_PROVIDER: z.enum(["openai", "none"]).default("openai"),
|
|
140
|
+
SUMMARY_API_KEY: z.string().min(1).optional(),
|
|
141
|
+
SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
|
|
142
|
+
LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
|
|
143
|
+
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info")
|
|
144
|
+
});
|
|
145
|
+
ConfigError = class extends Error {
|
|
146
|
+
constructor(message) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "ConfigError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// src/db/schema.ts
|
|
155
|
+
var schema_exports = {};
|
|
156
|
+
__export(schema_exports, {
|
|
157
|
+
packages: () => packages,
|
|
158
|
+
seedPackages: () => seedPackages,
|
|
159
|
+
syncRuns: () => syncRuns
|
|
160
|
+
});
|
|
161
|
+
import {
|
|
162
|
+
bigint,
|
|
163
|
+
boolean,
|
|
164
|
+
index,
|
|
165
|
+
integer,
|
|
166
|
+
jsonb,
|
|
167
|
+
pgTable,
|
|
168
|
+
real,
|
|
169
|
+
serial,
|
|
170
|
+
text,
|
|
171
|
+
timestamp,
|
|
172
|
+
vector
|
|
173
|
+
} from "drizzle-orm/pg-core";
|
|
174
|
+
var ts, packages, syncRuns, seedPackages;
|
|
175
|
+
var init_schema = __esm({
|
|
176
|
+
"src/db/schema.ts"() {
|
|
177
|
+
"use strict";
|
|
178
|
+
init_esm_shims();
|
|
179
|
+
init_constants();
|
|
180
|
+
ts = (name) => timestamp(name, { withTimezone: true, mode: "date" });
|
|
181
|
+
packages = pgTable(
|
|
182
|
+
"packages",
|
|
183
|
+
{
|
|
184
|
+
id: serial("id").primaryKey(),
|
|
185
|
+
name: text("name").notNull().unique(),
|
|
186
|
+
ecosystem: text("ecosystem").notNull().default("npm"),
|
|
187
|
+
// Classification + descriptive text
|
|
188
|
+
category: text("category").$type(),
|
|
189
|
+
description: text("description"),
|
|
190
|
+
summary: text("summary"),
|
|
191
|
+
repoUrl: text("repo_url"),
|
|
192
|
+
homepage: text("homepage"),
|
|
193
|
+
latestVersion: text("latest_version"),
|
|
194
|
+
license: text("license"),
|
|
195
|
+
// Lifecycle flags
|
|
196
|
+
deprecated: boolean("deprecated").notNull().default(false),
|
|
197
|
+
archived: boolean("archived").notNull().default(false),
|
|
198
|
+
// Age / maintenance signals
|
|
199
|
+
firstPublishedAt: ts("first_published_at"),
|
|
200
|
+
lastReleaseAt: ts("last_release_at"),
|
|
201
|
+
// Adoption signals
|
|
202
|
+
weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
|
|
203
|
+
downloadGrowth90d: real("download_growth_90d"),
|
|
204
|
+
dependentsCount: integer("dependents_count"),
|
|
205
|
+
stars: integer("stars"),
|
|
206
|
+
openIssues: integer("open_issues"),
|
|
207
|
+
closedIssues: integer("closed_issues"),
|
|
208
|
+
// Reliability / efficiency signals
|
|
209
|
+
scorecard: real("scorecard"),
|
|
210
|
+
bundleMinGzipKb: real("bundle_min_gzip_kb"),
|
|
211
|
+
advisories: jsonb("advisories").$type(),
|
|
212
|
+
// Computed outputs
|
|
213
|
+
healthScore: integer("health_score"),
|
|
214
|
+
confidence: text("confidence").$type(),
|
|
215
|
+
scoreBreakdown: jsonb("score_breakdown").$type(),
|
|
216
|
+
usageGuide: jsonb("usage_guide").$type(),
|
|
217
|
+
embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
|
|
218
|
+
// Freshness + bookkeeping
|
|
219
|
+
dataAsOf: timestamp("data_as_of", { withTimezone: true, mode: "date" }),
|
|
220
|
+
createdAt: ts("created_at").notNull().defaultNow(),
|
|
221
|
+
updatedAt: ts("updated_at").notNull().defaultNow()
|
|
222
|
+
},
|
|
223
|
+
(table2) => [
|
|
224
|
+
index("packages_category_idx").on(table2.category),
|
|
225
|
+
index("packages_health_score_idx").on(table2.healthScore),
|
|
226
|
+
// pgvector HNSW index for cosine similarity search (§11).
|
|
227
|
+
index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops"))
|
|
228
|
+
]
|
|
229
|
+
);
|
|
230
|
+
syncRuns = pgTable("sync_runs", {
|
|
231
|
+
id: serial("id").primaryKey(),
|
|
232
|
+
startedAt: ts("started_at").notNull().defaultNow(),
|
|
233
|
+
finishedAt: ts("finished_at"),
|
|
234
|
+
packagesSeen: integer("packages_seen").notNull().default(0),
|
|
235
|
+
packagesUpdated: integer("packages_updated").notNull().default(0),
|
|
236
|
+
errors: jsonb("errors").$type().notNull().default([]),
|
|
237
|
+
status: text("status").$type().notNull().default("running")
|
|
238
|
+
});
|
|
239
|
+
seedPackages = pgTable("seed_packages", {
|
|
240
|
+
name: text("name").primaryKey(),
|
|
241
|
+
category: text("category").$type(),
|
|
242
|
+
addedAt: ts("added_at").notNull().defaultNow()
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// src/db/client.ts
|
|
248
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
249
|
+
import postgres from "postgres";
|
|
250
|
+
function createDb(opts = {}) {
|
|
251
|
+
const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
|
|
252
|
+
const sql6 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
|
|
253
|
+
} });
|
|
254
|
+
const db = drizzle(sql6, { schema: schema_exports });
|
|
255
|
+
return { db, sql: sql6, close: () => sql6.end() };
|
|
256
|
+
}
|
|
257
|
+
var init_client = __esm({
|
|
258
|
+
"src/db/client.ts"() {
|
|
259
|
+
"use strict";
|
|
260
|
+
init_esm_shims();
|
|
261
|
+
init_config();
|
|
262
|
+
init_schema();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/core/logger.ts
|
|
267
|
+
function currentLevel() {
|
|
268
|
+
const raw = process.env.LOG_LEVEL;
|
|
269
|
+
return raw && raw in LEVELS ? LEVELS[raw] : LEVELS.info;
|
|
270
|
+
}
|
|
271
|
+
function emit(level, args) {
|
|
272
|
+
if (LEVELS[level] > currentLevel()) return;
|
|
273
|
+
const parts = args.map(
|
|
274
|
+
(a) => typeof a === "string" ? a : a instanceof Error ? a.stack ?? a.message : safeStringify(a)
|
|
275
|
+
);
|
|
276
|
+
process.stderr.write(`[lurq] ${level.toUpperCase()} ${parts.join(" ")}
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
function safeStringify(value) {
|
|
280
|
+
try {
|
|
281
|
+
return JSON.stringify(value);
|
|
282
|
+
} catch {
|
|
283
|
+
return String(value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
var LEVELS, logger;
|
|
287
|
+
var init_logger = __esm({
|
|
288
|
+
"src/core/logger.ts"() {
|
|
289
|
+
"use strict";
|
|
290
|
+
init_esm_shims();
|
|
291
|
+
LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
292
|
+
logger = {
|
|
293
|
+
error: (...args) => emit("error", args),
|
|
294
|
+
warn: (...args) => emit("warn", args),
|
|
295
|
+
info: (...args) => emit("info", args),
|
|
296
|
+
debug: (...args) => emit("debug", args)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// src/core/http.ts
|
|
302
|
+
import { createHash } from "crypto";
|
|
303
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
304
|
+
import { existsSync } from "fs";
|
|
305
|
+
import { homedir } from "os";
|
|
306
|
+
import { dirname, join } from "path";
|
|
307
|
+
function limiterFor(host) {
|
|
308
|
+
let limiter = limiters.get(host);
|
|
309
|
+
if (!limiter) {
|
|
310
|
+
limiter = new HostLimiter(HOST_CONFIG[host] ?? DEFAULT_HOST_CONFIG);
|
|
311
|
+
limiters.set(host, limiter);
|
|
312
|
+
}
|
|
313
|
+
return limiter;
|
|
314
|
+
}
|
|
315
|
+
function setCacheBypassRead(value) {
|
|
316
|
+
bypassCacheRead = value;
|
|
317
|
+
}
|
|
318
|
+
function cacheDir() {
|
|
319
|
+
return process.env.LURQ_CACHE_DIR ?? join(homedir(), ".cache", "lurq", "http");
|
|
320
|
+
}
|
|
321
|
+
function cacheFile(key) {
|
|
322
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
323
|
+
return join(cacheDir(), `${hash}.json`);
|
|
324
|
+
}
|
|
325
|
+
async function readCache(key, ttlMs) {
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
const mem = memoryCache.get(key);
|
|
328
|
+
if (mem && now - mem.fetchedAt < ttlMs) return mem;
|
|
329
|
+
const file = cacheFile(key);
|
|
330
|
+
if (!existsSync(file)) return void 0;
|
|
331
|
+
try {
|
|
332
|
+
const entry = JSON.parse(await readFile(file, "utf8"));
|
|
333
|
+
if (now - entry.fetchedAt < ttlMs) {
|
|
334
|
+
memoryCache.set(key, entry);
|
|
335
|
+
return entry;
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
async function writeCache(key, entry) {
|
|
342
|
+
memoryCache.set(key, entry);
|
|
343
|
+
const file = cacheFile(key);
|
|
344
|
+
try {
|
|
345
|
+
await mkdir(dirname(file), { recursive: true });
|
|
346
|
+
await writeFile(file, JSON.stringify(entry), "utf8");
|
|
347
|
+
} catch (err) {
|
|
348
|
+
logger.debug(`http cache write failed: ${err.message}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function httpRequest(url, opts) {
|
|
352
|
+
const {
|
|
353
|
+
host,
|
|
354
|
+
ttlMs,
|
|
355
|
+
method = "GET",
|
|
356
|
+
headers = {},
|
|
357
|
+
body,
|
|
358
|
+
accept = "json",
|
|
359
|
+
timeoutMs = 15e3,
|
|
360
|
+
retries = 3,
|
|
361
|
+
fetchImpl = fetch
|
|
362
|
+
} = opts;
|
|
363
|
+
const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
|
|
364
|
+
if (ttlMs > 0 && !bypassCacheRead) {
|
|
365
|
+
const cached2 = await readCache(key, ttlMs);
|
|
366
|
+
if (cached2) {
|
|
367
|
+
return { status: cached2.status, data: decode(cached2.body, accept), fromCache: true };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const limiter = limiterFor(host);
|
|
371
|
+
let lastErr;
|
|
372
|
+
for (let attempt2 = 0; attempt2 <= retries; attempt2++) {
|
|
373
|
+
await limiter.acquire();
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetchImpl(url, { method, headers, body, signal: controller.signal });
|
|
378
|
+
const text2 = await res.text();
|
|
379
|
+
if (res.ok) {
|
|
380
|
+
if (ttlMs > 0) await writeCache(key, { fetchedAt: Date.now(), status: res.status, body: text2 });
|
|
381
|
+
return { status: res.status, data: decode(text2, accept), fromCache: false };
|
|
382
|
+
}
|
|
383
|
+
if (RETRYABLE.has(res.status) && attempt2 < retries) {
|
|
384
|
+
const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
|
|
385
|
+
await delay(retryAfter ?? backoff(attempt2));
|
|
386
|
+
lastErr = new HttpError(`HTTP ${res.status} for ${url}`, res.status, url);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
throw new HttpError(`HTTP ${res.status} for ${url}`, res.status, url);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
lastErr = err;
|
|
392
|
+
const isAbort = err instanceof Error && err.name === "AbortError";
|
|
393
|
+
const isHttp = err instanceof HttpError;
|
|
394
|
+
if (isHttp && !RETRYABLE.has(err.status)) throw err;
|
|
395
|
+
if (attempt2 < retries) {
|
|
396
|
+
await delay(backoff(attempt2));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (isAbort) throw new HttpError(`Timeout after ${timeoutMs}ms for ${url}`, 0, url);
|
|
400
|
+
} finally {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
limiter.release();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
throw lastErr instanceof Error ? lastErr : new HttpError(`Request failed for ${url}`, 0, url);
|
|
406
|
+
}
|
|
407
|
+
async function httpGetJson(url, opts) {
|
|
408
|
+
return httpRequest(url, { ...opts, method: "GET", accept: "json" });
|
|
409
|
+
}
|
|
410
|
+
function decode(text2, accept) {
|
|
411
|
+
if (accept === "text") return text2;
|
|
412
|
+
return text2 ? JSON.parse(text2) : null;
|
|
413
|
+
}
|
|
414
|
+
function backoff(attempt2) {
|
|
415
|
+
const base = 500 * 2 ** attempt2;
|
|
416
|
+
return base + Math.floor(attempt2 * 137 % 100 * 3);
|
|
417
|
+
}
|
|
418
|
+
function parseRetryAfter(value) {
|
|
419
|
+
if (!value) return void 0;
|
|
420
|
+
const seconds = Number(value);
|
|
421
|
+
if (!Number.isNaN(seconds)) return seconds * 1e3;
|
|
422
|
+
const date = Date.parse(value);
|
|
423
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
424
|
+
}
|
|
425
|
+
function delay(ms) {
|
|
426
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
427
|
+
}
|
|
428
|
+
var HttpError, DEFAULT_HOST_CONFIG, HOST_CONFIG, HostLimiter, limiters, memoryCache, bypassCacheRead, RETRYABLE;
|
|
429
|
+
var init_http = __esm({
|
|
430
|
+
"src/core/http.ts"() {
|
|
431
|
+
"use strict";
|
|
432
|
+
init_esm_shims();
|
|
433
|
+
init_logger();
|
|
434
|
+
HttpError = class extends Error {
|
|
435
|
+
constructor(message, status, url) {
|
|
436
|
+
super(message);
|
|
437
|
+
this.status = status;
|
|
438
|
+
this.url = url;
|
|
439
|
+
this.name = "HttpError";
|
|
440
|
+
}
|
|
441
|
+
status;
|
|
442
|
+
url;
|
|
443
|
+
};
|
|
444
|
+
DEFAULT_HOST_CONFIG = { maxConcurrent: 6, minIntervalMs: 0 };
|
|
445
|
+
HOST_CONFIG = {
|
|
446
|
+
"api.github.com": { maxConcurrent: 4, minIntervalMs: 50 },
|
|
447
|
+
"registry.npmjs.org": { maxConcurrent: 4, minIntervalMs: 50 },
|
|
448
|
+
// The downloads API rate-limits aggressively — serialize with a steady gap.
|
|
449
|
+
// Weekly downloads go through the bulk endpoint; only growth hits this often.
|
|
450
|
+
"api.npmjs.org": { maxConcurrent: 1, minIntervalMs: 250 },
|
|
451
|
+
"api.deps.dev": { maxConcurrent: 4, minIntervalMs: 40 },
|
|
452
|
+
"raw.githubusercontent.com": { maxConcurrent: 4, minIntervalMs: 30 },
|
|
453
|
+
// Bundlephobia is slow/flaky — be gentle (§9.5).
|
|
454
|
+
"bundlephobia.com": { maxConcurrent: 2, minIntervalMs: 200 }
|
|
455
|
+
};
|
|
456
|
+
HostLimiter = class {
|
|
457
|
+
constructor(config) {
|
|
458
|
+
this.config = config;
|
|
459
|
+
}
|
|
460
|
+
config;
|
|
461
|
+
active = 0;
|
|
462
|
+
nextAvailable = 0;
|
|
463
|
+
queue = [];
|
|
464
|
+
async acquire() {
|
|
465
|
+
if (this.active >= this.config.maxConcurrent) {
|
|
466
|
+
await new Promise((resolve) => this.queue.push(resolve));
|
|
467
|
+
}
|
|
468
|
+
this.active++;
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
const wait = Math.max(0, this.nextAvailable - now);
|
|
471
|
+
this.nextAvailable = Math.max(now, this.nextAvailable) + this.config.minIntervalMs;
|
|
472
|
+
if (wait > 0) await delay(wait);
|
|
473
|
+
}
|
|
474
|
+
release() {
|
|
475
|
+
this.active--;
|
|
476
|
+
const next = this.queue.shift();
|
|
477
|
+
if (next) next();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
limiters = /* @__PURE__ */ new Map();
|
|
481
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
482
|
+
bypassCacheRead = false;
|
|
483
|
+
RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// src/ingestion/sources/npmRegistry.ts
|
|
488
|
+
function encodeNpmName(name) {
|
|
489
|
+
return name.startsWith("@") ? name.replace("/", "%2F") : name;
|
|
490
|
+
}
|
|
491
|
+
function parseGithubRepo(url) {
|
|
492
|
+
if (!url) return null;
|
|
493
|
+
const match = url.match(/github\.com[/:]([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/#?].*)?$/i);
|
|
494
|
+
if (!match) return null;
|
|
495
|
+
const owner = match[1];
|
|
496
|
+
const repo = match[2];
|
|
497
|
+
if (!owner || !repo) return null;
|
|
498
|
+
return { owner, repo };
|
|
499
|
+
}
|
|
500
|
+
function normalizeRepoUrl(repository, homepage) {
|
|
501
|
+
let raw = null;
|
|
502
|
+
if (typeof repository === "string") raw = repository;
|
|
503
|
+
else if (repository && typeof repository === "object" && "url" in repository) {
|
|
504
|
+
raw = String(repository.url);
|
|
505
|
+
}
|
|
506
|
+
const repo = parseGithubRepo(raw) ?? parseGithubRepo(homepage);
|
|
507
|
+
const repoUrl = repo ? `https://github.com/${repo.owner}/${repo.repo}` : cleanUrl(raw) ?? null;
|
|
508
|
+
return { repoUrl, repo };
|
|
509
|
+
}
|
|
510
|
+
function cleanUrl(url) {
|
|
511
|
+
if (!url) return null;
|
|
512
|
+
return url.replace(/^git\+/, "").replace(/^git:\/\//, "https://").replace(/\.git$/, "");
|
|
513
|
+
}
|
|
514
|
+
function pickLicense(value) {
|
|
515
|
+
if (!value) return null;
|
|
516
|
+
if (typeof value === "string") return value;
|
|
517
|
+
if (typeof value === "object" && "type" in value) return String(value.type);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
function toDate(value) {
|
|
521
|
+
if (typeof value !== "string") return null;
|
|
522
|
+
const ms = Date.parse(value);
|
|
523
|
+
return Number.isNaN(ms) ? null : new Date(ms);
|
|
524
|
+
}
|
|
525
|
+
function parseNpmRegistry(json2) {
|
|
526
|
+
const latest = json2?.["dist-tags"]?.latest ?? null;
|
|
527
|
+
const latestManifest = latest ? json2?.versions?.[latest] ?? {} : {};
|
|
528
|
+
const time = json2?.time ?? {};
|
|
529
|
+
const repository = json2?.repository ?? latestManifest?.repository;
|
|
530
|
+
const homepage = json2?.homepage ?? latestManifest?.homepage ?? null;
|
|
531
|
+
const { repoUrl, repo } = normalizeRepoUrl(repository, homepage);
|
|
532
|
+
const deprecated = Boolean(latestManifest?.deprecated ?? json2?.deprecated);
|
|
533
|
+
const maintainers = Array.isArray(json2?.maintainers) ? json2.maintainers.length : null;
|
|
534
|
+
return {
|
|
535
|
+
name: json2?.name ?? "",
|
|
536
|
+
description: json2?.description ?? latestManifest?.description ?? null,
|
|
537
|
+
latestVersion: latest,
|
|
538
|
+
license: pickLicense(json2?.license ?? latestManifest?.license),
|
|
539
|
+
homepage,
|
|
540
|
+
repo,
|
|
541
|
+
repoUrl,
|
|
542
|
+
firstPublishedAt: toDate(time?.created),
|
|
543
|
+
lastReleaseAt: toDate(latest ? time?.[latest] : null) ?? toDate(time?.modified),
|
|
544
|
+
deprecated,
|
|
545
|
+
maintainersCount: maintainers,
|
|
546
|
+
readme: typeof json2?.readme === "string" ? json2.readme : null
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async function fetchNpmRegistry(name, fetchImpl) {
|
|
550
|
+
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
551
|
+
const { data } = await httpGetJson(url, {
|
|
552
|
+
host: HOST,
|
|
553
|
+
ttlMs: CACHE_TTL.npmRegistry,
|
|
554
|
+
fetchImpl
|
|
555
|
+
});
|
|
556
|
+
return parseNpmRegistry(data);
|
|
557
|
+
}
|
|
558
|
+
async function npmPackageExists(name, fetchImpl) {
|
|
559
|
+
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
560
|
+
try {
|
|
561
|
+
const { status } = await httpGetJson(url, {
|
|
562
|
+
host: HOST,
|
|
563
|
+
ttlMs: 0,
|
|
564
|
+
retries: 1,
|
|
565
|
+
fetchImpl
|
|
566
|
+
});
|
|
567
|
+
return status === 200;
|
|
568
|
+
} catch (err) {
|
|
569
|
+
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
throw err;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
var HOST;
|
|
576
|
+
var init_npmRegistry = __esm({
|
|
577
|
+
"src/ingestion/sources/npmRegistry.ts"() {
|
|
578
|
+
"use strict";
|
|
579
|
+
init_esm_shims();
|
|
580
|
+
init_constants();
|
|
581
|
+
init_http();
|
|
582
|
+
HOST = "registry.npmjs.org";
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// src/ingestion/sources/npmDownloads.ts
|
|
587
|
+
function parseWeeklyDownloads(json2) {
|
|
588
|
+
const value = json2?.downloads;
|
|
589
|
+
return typeof value === "number" ? value : null;
|
|
590
|
+
}
|
|
591
|
+
function parseDownloadGrowth(json2) {
|
|
592
|
+
const points = Array.isArray(json2?.downloads) ? json2.downloads : [];
|
|
593
|
+
if (points.length < 60) return null;
|
|
594
|
+
const tail = points.slice(-60);
|
|
595
|
+
const prior = tail.slice(0, 30);
|
|
596
|
+
const recent = tail.slice(30);
|
|
597
|
+
const sum = (arr) => arr.reduce((acc, p) => acc + (p.downloads || 0), 0);
|
|
598
|
+
const priorAvg = sum(prior) / prior.length;
|
|
599
|
+
const recentAvg = sum(recent) / recent.length;
|
|
600
|
+
if (priorAvg <= 0) return recentAvg > 0 ? 1 : 0;
|
|
601
|
+
return (recentAvg - priorAvg) / priorAvg;
|
|
602
|
+
}
|
|
603
|
+
function ymd(date) {
|
|
604
|
+
return date.toISOString().slice(0, 10);
|
|
605
|
+
}
|
|
606
|
+
function last90DayRange(now = /* @__PURE__ */ new Date()) {
|
|
607
|
+
const end = now;
|
|
608
|
+
const start = new Date(end.getTime() - 90 * 24 * 60 * 60 * 1e3);
|
|
609
|
+
return `${ymd(start)}:${ymd(end)}`;
|
|
610
|
+
}
|
|
611
|
+
function parseBulkWeekly(json2, requested) {
|
|
612
|
+
const map = /* @__PURE__ */ new Map();
|
|
613
|
+
if (json2 && typeof json2.downloads === "number" && typeof json2.package === "string") {
|
|
614
|
+
map.set(json2.package, json2.downloads);
|
|
615
|
+
return map;
|
|
616
|
+
}
|
|
617
|
+
for (const name of requested) {
|
|
618
|
+
const entry = json2?.[name];
|
|
619
|
+
map.set(name, entry && typeof entry.downloads === "number" ? entry.downloads : null);
|
|
620
|
+
}
|
|
621
|
+
return map;
|
|
622
|
+
}
|
|
623
|
+
function chunk(items, size) {
|
|
624
|
+
const out = [];
|
|
625
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
async function fetchWeeklyDownloads(name, fetchImpl) {
|
|
629
|
+
try {
|
|
630
|
+
const { data } = await httpGetJson(
|
|
631
|
+
`https://${HOST2}/downloads/point/last-week/${name}`,
|
|
632
|
+
{ host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
633
|
+
);
|
|
634
|
+
return parseWeeklyDownloads(data);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err instanceof HttpError && err.status === 404) return null;
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function fetchDownloadGrowth(name, fetchImpl) {
|
|
641
|
+
try {
|
|
642
|
+
const { data } = await httpGetJson(
|
|
643
|
+
`https://${HOST2}/downloads/range/${last90DayRange()}/${name}`,
|
|
644
|
+
{ host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
|
|
645
|
+
);
|
|
646
|
+
return parseDownloadGrowth(data);
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function fetchBulkWeeklyDownloads(names, fetchImpl) {
|
|
652
|
+
const result = /* @__PURE__ */ new Map();
|
|
653
|
+
const unscoped = names.filter((n) => !n.startsWith("@"));
|
|
654
|
+
const scoped = names.filter((n) => n.startsWith("@"));
|
|
655
|
+
for (const batch of chunk(unscoped, BULK_CHUNK)) {
|
|
656
|
+
try {
|
|
657
|
+
const { data } = await httpGetJson(
|
|
658
|
+
`https://${HOST2}/downloads/point/last-week/${batch.join(",")}`,
|
|
659
|
+
{ host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
660
|
+
);
|
|
661
|
+
for (const [k, v] of parseBulkWeekly(data, batch)) result.set(k, v);
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
for (const name of scoped) {
|
|
666
|
+
try {
|
|
667
|
+
result.set(name, await fetchWeeklyDownloads(name, fetchImpl));
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
var HOST2, BULK_CHUNK;
|
|
674
|
+
var init_npmDownloads = __esm({
|
|
675
|
+
"src/ingestion/sources/npmDownloads.ts"() {
|
|
676
|
+
"use strict";
|
|
677
|
+
init_esm_shims();
|
|
678
|
+
init_constants();
|
|
679
|
+
init_http();
|
|
680
|
+
HOST2 = "api.npmjs.org";
|
|
681
|
+
BULK_CHUNK = 128;
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// src/ingestion/sources/github.ts
|
|
686
|
+
function parseGithub(json2, now = /* @__PURE__ */ new Date()) {
|
|
687
|
+
const repo = json2?.data?.repository;
|
|
688
|
+
if (!repo) return null;
|
|
689
|
+
const releaseNodes = repo.releases?.nodes ?? [];
|
|
690
|
+
const releaseDates = releaseNodes.map((n) => Date.parse(n.createdAt)).filter((ms) => !Number.isNaN(ms));
|
|
691
|
+
const cutoff = now.getTime() - ONE_YEAR_MS;
|
|
692
|
+
return {
|
|
693
|
+
stars: typeof repo.stargazerCount === "number" ? repo.stargazerCount : null,
|
|
694
|
+
openIssues: repo.openIssues?.totalCount ?? null,
|
|
695
|
+
closedIssues: repo.closedIssues?.totalCount ?? null,
|
|
696
|
+
archived: Boolean(repo.isArchived),
|
|
697
|
+
lastReleaseAt: releaseDates.length ? new Date(Math.max(...releaseDates)) : null,
|
|
698
|
+
releasesLast12mo: releaseDates.filter((ms) => ms >= cutoff).length
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async function fetchGithubRepo(owner, repo, token, fetchImpl) {
|
|
702
|
+
const body = JSON.stringify({ query: QUERY, variables: { owner, name: repo } });
|
|
703
|
+
const { data } = await httpRequest(ENDPOINT, {
|
|
704
|
+
host: HOST3,
|
|
705
|
+
ttlMs: CACHE_TTL.github,
|
|
706
|
+
method: "POST",
|
|
707
|
+
headers: {
|
|
708
|
+
Authorization: `bearer ${token}`,
|
|
709
|
+
"Content-Type": "application/json",
|
|
710
|
+
"User-Agent": "lurq"
|
|
711
|
+
},
|
|
712
|
+
body,
|
|
713
|
+
// Keep the auth token out of the cache key.
|
|
714
|
+
cacheKey: `POST ${ENDPOINT} ${owner}/${repo}`,
|
|
715
|
+
fetchImpl
|
|
716
|
+
});
|
|
717
|
+
return parseGithub(data);
|
|
718
|
+
}
|
|
719
|
+
var HOST3, ENDPOINT, QUERY, ONE_YEAR_MS;
|
|
720
|
+
var init_github = __esm({
|
|
721
|
+
"src/ingestion/sources/github.ts"() {
|
|
722
|
+
"use strict";
|
|
723
|
+
init_esm_shims();
|
|
724
|
+
init_constants();
|
|
725
|
+
init_http();
|
|
726
|
+
HOST3 = "api.github.com";
|
|
727
|
+
ENDPOINT = `https://${HOST3}/graphql`;
|
|
728
|
+
QUERY = `query($owner:String!, $name:String!) {
|
|
729
|
+
repository(owner:$owner, name:$name) {
|
|
730
|
+
stargazerCount
|
|
731
|
+
isArchived
|
|
732
|
+
openIssues: issues(states:OPEN) { totalCount }
|
|
733
|
+
closedIssues: issues(states:CLOSED) { totalCount }
|
|
734
|
+
releases(first: 50, orderBy: {field: CREATED_AT, direction: DESC}) {
|
|
735
|
+
nodes { createdAt }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}`;
|
|
739
|
+
ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// src/ingestion/sources/depsDev.ts
|
|
744
|
+
function encodeName(name) {
|
|
745
|
+
return encodeURIComponent(name);
|
|
746
|
+
}
|
|
747
|
+
function severityFromCvss(score) {
|
|
748
|
+
if (score == null) return "moderate";
|
|
749
|
+
if (score >= 9) return "critical";
|
|
750
|
+
if (score >= 7) return "high";
|
|
751
|
+
if (score >= 4) return "moderate";
|
|
752
|
+
if (score > 0) return "low";
|
|
753
|
+
return "info";
|
|
754
|
+
}
|
|
755
|
+
function parseScorecard(json2) {
|
|
756
|
+
const score = json2?.scorecard?.overallScore;
|
|
757
|
+
return typeof score === "number" ? score : null;
|
|
758
|
+
}
|
|
759
|
+
function parseAdvisoryKeys(versionJson) {
|
|
760
|
+
const keys = versionJson?.advisoryKeys;
|
|
761
|
+
if (!Array.isArray(keys)) return [];
|
|
762
|
+
return keys.map((k) => typeof k === "string" ? k : k?.id).filter((id) => typeof id === "string");
|
|
763
|
+
}
|
|
764
|
+
function parseAdvisoryDetail(json2) {
|
|
765
|
+
return {
|
|
766
|
+
id: json2?.advisoryKey?.id ?? json2?.advisoryKey ?? "unknown",
|
|
767
|
+
severity: severityFromCvss(json2?.cvss3Score),
|
|
768
|
+
summary: json2?.title ?? ""
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
async function fetchScorecard(owner, repo, fetchImpl) {
|
|
772
|
+
const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
|
|
773
|
+
const url = `https://${HOST4}/v3/projects/${projectKey}`;
|
|
774
|
+
try {
|
|
775
|
+
const { data } = await httpGetJson(url, {
|
|
776
|
+
host: HOST4,
|
|
777
|
+
ttlMs: CACHE_TTL.depsDev,
|
|
778
|
+
fetchImpl
|
|
779
|
+
});
|
|
780
|
+
return parseScorecard(data);
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function fetchAdvisories(name, version, fetchImpl) {
|
|
786
|
+
const versionUrl = `https://${HOST4}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
|
|
787
|
+
let keys = [];
|
|
788
|
+
try {
|
|
789
|
+
const { data } = await httpGetJson(versionUrl, {
|
|
790
|
+
host: HOST4,
|
|
791
|
+
ttlMs: CACHE_TTL.depsDev,
|
|
792
|
+
fetchImpl
|
|
793
|
+
});
|
|
794
|
+
keys = parseAdvisoryKeys(data).slice(0, MAX_ADVISORIES);
|
|
795
|
+
} catch {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
const details = await Promise.allSettled(
|
|
799
|
+
keys.map(
|
|
800
|
+
(key) => httpGetJson(`https://${HOST4}/v3/advisories/${encodeURIComponent(key)}`, {
|
|
801
|
+
host: HOST4,
|
|
802
|
+
ttlMs: CACHE_TTL.depsDev,
|
|
803
|
+
fetchImpl
|
|
804
|
+
})
|
|
805
|
+
)
|
|
806
|
+
);
|
|
807
|
+
return details.filter(
|
|
808
|
+
(d) => d.status === "fulfilled"
|
|
809
|
+
).map((d) => parseAdvisoryDetail(d.value.data));
|
|
810
|
+
}
|
|
811
|
+
async function fetchDepsDev(name, version, repo, fetchImpl) {
|
|
812
|
+
const [scorecard, advisories] = await Promise.all([
|
|
813
|
+
repo ? fetchScorecard(repo.owner, repo.repo, fetchImpl) : Promise.resolve(null),
|
|
814
|
+
version ? fetchAdvisories(name, version, fetchImpl) : Promise.resolve([])
|
|
815
|
+
]);
|
|
816
|
+
return { scorecard, advisories };
|
|
817
|
+
}
|
|
818
|
+
var HOST4, MAX_ADVISORIES;
|
|
819
|
+
var init_depsDev = __esm({
|
|
820
|
+
"src/ingestion/sources/depsDev.ts"() {
|
|
821
|
+
"use strict";
|
|
822
|
+
init_esm_shims();
|
|
823
|
+
init_constants();
|
|
824
|
+
init_http();
|
|
825
|
+
HOST4 = "api.deps.dev";
|
|
826
|
+
MAX_ADVISORIES = 10;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// src/ingestion/sources/bundlephobia.ts
|
|
831
|
+
function parseBundleSize(json2) {
|
|
832
|
+
const gzipBytes = json2?.gzip;
|
|
833
|
+
if (typeof gzipBytes !== "number") return null;
|
|
834
|
+
return Math.round(gzipBytes / 1024 * 10) / 10;
|
|
835
|
+
}
|
|
836
|
+
async function fetchBundlephobia(name, category, fetchImpl) {
|
|
837
|
+
if (!isFrontendCategory(category)) return { bundleMinGzipKb: null };
|
|
838
|
+
const url = `https://${HOST5}/api/size?package=${encodeURIComponent(name)}`;
|
|
839
|
+
try {
|
|
840
|
+
const { data } = await httpGetJson(url, {
|
|
841
|
+
host: HOST5,
|
|
842
|
+
ttlMs: CACHE_TTL.bundlephobia,
|
|
843
|
+
timeoutMs: 12e3,
|
|
844
|
+
retries: 1,
|
|
845
|
+
fetchImpl
|
|
846
|
+
});
|
|
847
|
+
return { bundleMinGzipKb: parseBundleSize(data) };
|
|
848
|
+
} catch {
|
|
849
|
+
return { bundleMinGzipKb: null };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
var HOST5;
|
|
853
|
+
var init_bundlephobia = __esm({
|
|
854
|
+
"src/ingestion/sources/bundlephobia.ts"() {
|
|
855
|
+
"use strict";
|
|
856
|
+
init_esm_shims();
|
|
857
|
+
init_constants();
|
|
858
|
+
init_http();
|
|
859
|
+
init_types();
|
|
860
|
+
HOST5 = "bundlephobia.com";
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// src/ingestion/sources/index.ts
|
|
865
|
+
var init_sources = __esm({
|
|
866
|
+
"src/ingestion/sources/index.ts"() {
|
|
867
|
+
"use strict";
|
|
868
|
+
init_esm_shims();
|
|
869
|
+
init_npmRegistry();
|
|
870
|
+
init_npmDownloads();
|
|
871
|
+
init_github();
|
|
872
|
+
init_depsDev();
|
|
873
|
+
init_bundlephobia();
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// src/ingestion/sources/githubReadme.ts
|
|
878
|
+
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
879
|
+
for (const file of CANDIDATES) {
|
|
880
|
+
const url = `https://${HOST6}/${owner}/${repo}/HEAD/${file}`;
|
|
881
|
+
try {
|
|
882
|
+
const { data } = await httpRequest(url, {
|
|
883
|
+
host: HOST6,
|
|
884
|
+
ttlMs: CACHE_TTL.github,
|
|
885
|
+
accept: "text",
|
|
886
|
+
retries: 0,
|
|
887
|
+
fetchImpl
|
|
888
|
+
});
|
|
889
|
+
if (data && data.trim().length > 0) return data;
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
var HOST6, CANDIDATES;
|
|
896
|
+
var init_githubReadme = __esm({
|
|
897
|
+
"src/ingestion/sources/githubReadme.ts"() {
|
|
898
|
+
"use strict";
|
|
899
|
+
init_esm_shims();
|
|
900
|
+
init_constants();
|
|
901
|
+
init_http();
|
|
902
|
+
HOST6 = "raw.githubusercontent.com";
|
|
903
|
+
CANDIDATES = ["README.md", "readme.md", "README.markdown", "README"];
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// src/ingestion/summarize.ts
|
|
908
|
+
function context7Hint(name) {
|
|
909
|
+
return `For the exact current API of "${name}", resolve it via Context7 (resolve-library-id \u2192 query-docs).`;
|
|
910
|
+
}
|
|
911
|
+
function truncateSentences(text2, max = 3) {
|
|
912
|
+
const clean = text2.replace(/\s+/g, " ").trim();
|
|
913
|
+
if (!clean) return "";
|
|
914
|
+
const sentences = clean.match(/[^.!?]+[.!?]+|[^.!?]+$/g) ?? [clean];
|
|
915
|
+
return sentences.slice(0, max).map((s) => s.trim()).join(" ").trim();
|
|
916
|
+
}
|
|
917
|
+
function buildUserPrompt(input) {
|
|
918
|
+
const readme = (input.readme ?? "").slice(0, 4e3);
|
|
919
|
+
return [
|
|
920
|
+
`Package: ${input.name}`,
|
|
921
|
+
`Category: ${input.category ?? "unknown"}`,
|
|
922
|
+
`Description: ${input.description ?? "(none)"}`,
|
|
923
|
+
`README (truncated):
|
|
924
|
+
${readme || "(none)"}`,
|
|
925
|
+
"",
|
|
926
|
+
"Return JSON with keys:",
|
|
927
|
+
'- "summary": 2-3 sentences: what it is, when to use it, how it fits an app.',
|
|
928
|
+
'- "whatItIs": one sentence.',
|
|
929
|
+
'- "whenToUse": one sentence on the ideal use case.',
|
|
930
|
+
'- "whenNotToUse": one sentence on when to pick something else (or "").',
|
|
931
|
+
'- "whereItFits": one sentence on its architectural role.',
|
|
932
|
+
'- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".'
|
|
933
|
+
].join("\n");
|
|
934
|
+
}
|
|
935
|
+
function str(value) {
|
|
936
|
+
return typeof value === "string" ? value.trim() : "";
|
|
937
|
+
}
|
|
938
|
+
function createSummaryProvider(fetchImpl) {
|
|
939
|
+
const config = getConfig();
|
|
940
|
+
if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
|
|
941
|
+
return new OpenAISummaryProvider(config.SUMMARY_API_KEY, config.SUMMARY_MODEL, fetchImpl);
|
|
942
|
+
}
|
|
943
|
+
return new FallbackSummaryProvider();
|
|
944
|
+
}
|
|
945
|
+
async function buildSummaryInput(signals, category, fetchImpl) {
|
|
946
|
+
const registry = signals.registry;
|
|
947
|
+
let readme = registry?.readme?.trim() || null;
|
|
948
|
+
if (!readme && registry?.repo) {
|
|
949
|
+
readme = await fetchGithubReadme(registry.repo.owner, registry.repo.repo, fetchImpl).catch(
|
|
950
|
+
() => null
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
name: signals.name,
|
|
955
|
+
description: registry?.description ?? null,
|
|
956
|
+
category,
|
|
957
|
+
readme,
|
|
958
|
+
repoUrl: registry?.repoUrl ?? null
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
var CATEGORY_ROLE, FallbackSummaryProvider, SYSTEM_PROMPT, OpenAISummaryProvider;
|
|
962
|
+
var init_summarize = __esm({
|
|
963
|
+
"src/ingestion/summarize.ts"() {
|
|
964
|
+
"use strict";
|
|
965
|
+
init_esm_shims();
|
|
966
|
+
init_config();
|
|
967
|
+
init_http();
|
|
968
|
+
init_logger();
|
|
969
|
+
init_githubReadme();
|
|
970
|
+
CATEGORY_ROLE = {
|
|
971
|
+
framework: "the core application framework your app is built on",
|
|
972
|
+
"meta-framework": "the full-stack framework wrapping routing, rendering, and build",
|
|
973
|
+
"state-management": "the client state layer shared across components",
|
|
974
|
+
routing: "the routing layer mapping URLs to views",
|
|
975
|
+
orm: "the data-access layer between your code and the database",
|
|
976
|
+
"database-client": "the low-level driver that talks to your database",
|
|
977
|
+
"ui-component-library": "the prebuilt UI component layer",
|
|
978
|
+
styling: "the styling layer for your components",
|
|
979
|
+
forms: "form state and submission handling",
|
|
980
|
+
validation: "input/schema validation at your boundaries",
|
|
981
|
+
"data-fetching": "the server-state/data-fetching layer",
|
|
982
|
+
"http-client": "the HTTP request layer for calling APIs",
|
|
983
|
+
auth: "authentication and session handling",
|
|
984
|
+
testing: "the test tooling layer",
|
|
985
|
+
"build-tool": "the build/dev pipeline",
|
|
986
|
+
bundler: "the module bundling step",
|
|
987
|
+
linting: "code-quality and formatting checks",
|
|
988
|
+
"date-time": "date and time handling",
|
|
989
|
+
animation: "UI animation and motion",
|
|
990
|
+
charts: "data visualization and charts",
|
|
991
|
+
i18n: "internationalization and localization",
|
|
992
|
+
utility: "a focused helper you compose into your app",
|
|
993
|
+
other: "a supporting library in your app"
|
|
994
|
+
};
|
|
995
|
+
FallbackSummaryProvider = class {
|
|
996
|
+
kind = "fallback";
|
|
997
|
+
async generate(input) {
|
|
998
|
+
const description = input.description?.trim() || null;
|
|
999
|
+
const role = CATEGORY_ROLE[input.category ?? "other"] ?? CATEGORY_ROLE.other;
|
|
1000
|
+
const summary = description ? truncateSentences(description, 3) : null;
|
|
1001
|
+
const guide = {
|
|
1002
|
+
whatItIs: description ? truncateSentences(description, 2) : input.name,
|
|
1003
|
+
whenToUse: `Reach for ${input.name} when you need ${role}.`,
|
|
1004
|
+
whereItFits: `Sits at ${role}.`,
|
|
1005
|
+
context7Hint: context7Hint(input.name)
|
|
1006
|
+
};
|
|
1007
|
+
return { summary, usageGuide: guide };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
SYSTEM_PROMPT = "You summarize npm packages factually for an AI coding agent choosing dependencies. Use ONLY the provided README/description. Never invent capabilities or APIs. Be concise and concrete. Respond with a JSON object.";
|
|
1011
|
+
OpenAISummaryProvider = class {
|
|
1012
|
+
constructor(apiKey, model, fetchImpl) {
|
|
1013
|
+
this.apiKey = apiKey;
|
|
1014
|
+
this.model = model;
|
|
1015
|
+
this.fetchImpl = fetchImpl;
|
|
1016
|
+
}
|
|
1017
|
+
apiKey;
|
|
1018
|
+
model;
|
|
1019
|
+
fetchImpl;
|
|
1020
|
+
kind = "openai";
|
|
1021
|
+
async generate(input) {
|
|
1022
|
+
try {
|
|
1023
|
+
const body = JSON.stringify({
|
|
1024
|
+
model: this.model,
|
|
1025
|
+
messages: [
|
|
1026
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1027
|
+
{ role: "user", content: buildUserPrompt(input) }
|
|
1028
|
+
],
|
|
1029
|
+
response_format: { type: "json_object" },
|
|
1030
|
+
temperature: 0.2
|
|
1031
|
+
});
|
|
1032
|
+
const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
|
|
1033
|
+
host: "api.openai.com",
|
|
1034
|
+
method: "POST",
|
|
1035
|
+
ttlMs: 30 * 24 * 60 * 60 * 1e3,
|
|
1036
|
+
// cache summaries 30d to control cost
|
|
1037
|
+
cacheKey: `openai-summary ${this.model} ${input.name} ${(input.readme ?? "").length}`,
|
|
1038
|
+
headers: {
|
|
1039
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1040
|
+
"Content-Type": "application/json"
|
|
1041
|
+
},
|
|
1042
|
+
body,
|
|
1043
|
+
fetchImpl: this.fetchImpl
|
|
1044
|
+
});
|
|
1045
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
1046
|
+
const parsed = content ? JSON.parse(content) : {};
|
|
1047
|
+
const guide = {
|
|
1048
|
+
whatItIs: str(parsed.whatItIs) || input.name,
|
|
1049
|
+
whenToUse: str(parsed.whenToUse),
|
|
1050
|
+
whenNotToUse: str(parsed.whenNotToUse) || void 0,
|
|
1051
|
+
whereItFits: str(parsed.whereItFits),
|
|
1052
|
+
howToWireIn: str(parsed.howToWireIn) || void 0,
|
|
1053
|
+
context7Hint: context7Hint(input.name)
|
|
1054
|
+
};
|
|
1055
|
+
return { summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null, usageGuide: guide };
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
logger.warn(`summary LLM failed for ${input.name}, using fallback: ${err.message}`);
|
|
1058
|
+
return new FallbackSummaryProvider().generate(input);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// src/ingestion/collect.ts
|
|
1066
|
+
async function attempt(source, errors, fn) {
|
|
1067
|
+
try {
|
|
1068
|
+
return await fn();
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1071
|
+
errors.push({ source, message });
|
|
1072
|
+
logger.debug(`[${source}] ${message}`);
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function collectSignals(name, category, deps = {}) {
|
|
1077
|
+
const { githubToken, fetchImpl } = deps;
|
|
1078
|
+
const errors = [];
|
|
1079
|
+
const registry = await attempt("npm-registry", errors, () => fetchNpmRegistry(name, fetchImpl));
|
|
1080
|
+
const repo = registry?.repo ?? null;
|
|
1081
|
+
const version = registry?.latestVersion ?? null;
|
|
1082
|
+
const [downloads, github, depsDev, bundle] = await Promise.all([
|
|
1083
|
+
attempt("npm-downloads", errors, async () => {
|
|
1084
|
+
const weeklyDownloads = deps.prefetchedWeekly !== void 0 ? deps.prefetchedWeekly : await fetchWeeklyDownloads(name, fetchImpl);
|
|
1085
|
+
const downloadGrowth90d = await fetchDownloadGrowth(name, fetchImpl);
|
|
1086
|
+
return { weeklyDownloads, downloadGrowth90d };
|
|
1087
|
+
}),
|
|
1088
|
+
repo && githubToken ? attempt("github", errors, () => fetchGithubRepo(repo.owner, repo.repo, githubToken, fetchImpl)) : Promise.resolve(null),
|
|
1089
|
+
attempt("deps-dev", errors, () => fetchDepsDev(name, version, repo, fetchImpl)),
|
|
1090
|
+
attempt("bundlephobia", errors, () => fetchBundlephobia(name, category, fetchImpl))
|
|
1091
|
+
]);
|
|
1092
|
+
return { name, registry, downloads, github, depsDev, bundle, errors };
|
|
1093
|
+
}
|
|
1094
|
+
var init_collect = __esm({
|
|
1095
|
+
"src/ingestion/collect.ts"() {
|
|
1096
|
+
"use strict";
|
|
1097
|
+
init_esm_shims();
|
|
1098
|
+
init_logger();
|
|
1099
|
+
init_bundlephobia();
|
|
1100
|
+
init_depsDev();
|
|
1101
|
+
init_github();
|
|
1102
|
+
init_npmDownloads();
|
|
1103
|
+
init_npmRegistry();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// src/scoring/weights.ts
|
|
1108
|
+
var HEALTH_WEIGHTS, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, CONFIDENCE;
|
|
1109
|
+
var init_weights = __esm({
|
|
1110
|
+
"src/scoring/weights.ts"() {
|
|
1111
|
+
"use strict";
|
|
1112
|
+
init_esm_shims();
|
|
1113
|
+
HEALTH_WEIGHTS = {
|
|
1114
|
+
maintenance: 0.35,
|
|
1115
|
+
adoption: 0.3,
|
|
1116
|
+
reliability: 0.25,
|
|
1117
|
+
efficiency: 0.1
|
|
1118
|
+
};
|
|
1119
|
+
MAINTENANCE_WEIGHTS = {
|
|
1120
|
+
recency: 0.5,
|
|
1121
|
+
cadence: 0.25,
|
|
1122
|
+
closeRatio: 0.25
|
|
1123
|
+
};
|
|
1124
|
+
MAINTENANCE = {
|
|
1125
|
+
/** A release within this many days scores full recency. */
|
|
1126
|
+
freshDays: 30,
|
|
1127
|
+
/** Recency decays linearly to 0 by this many days (~24 months). */
|
|
1128
|
+
staleDays: 730,
|
|
1129
|
+
/** Releases/12mo at or above this cap full cadence. */
|
|
1130
|
+
cadenceCap: 12,
|
|
1131
|
+
/** deprecated/archived hard-cap maintenance at this value. */
|
|
1132
|
+
deadCap: 15
|
|
1133
|
+
};
|
|
1134
|
+
ADOPTION = {
|
|
1135
|
+
/** log10(weekly downloads): this maps to 0. */
|
|
1136
|
+
downloadsLogMin: 1,
|
|
1137
|
+
// 10/wk
|
|
1138
|
+
/** log10(weekly downloads): this maps to 100. */
|
|
1139
|
+
downloadsLogMax: 7,
|
|
1140
|
+
// 10M/wk
|
|
1141
|
+
/** log10(stars) that maps to 100. */
|
|
1142
|
+
starsLogMax: 5,
|
|
1143
|
+
// 100k stars
|
|
1144
|
+
/** Weight of the stars component when available (downloads gets the rest). */
|
|
1145
|
+
starsWeight: 0.2,
|
|
1146
|
+
/** Growth bonus: growth fraction × this, clamped to ±cap. */
|
|
1147
|
+
growthMultiplier: 30,
|
|
1148
|
+
growthBonusCap: 20
|
|
1149
|
+
};
|
|
1150
|
+
RELIABILITY = {
|
|
1151
|
+
/** Used when no Scorecard is available (unknown security posture). */
|
|
1152
|
+
neutralWhenUnknown: 50,
|
|
1153
|
+
/** Advisory penalties by severity. */
|
|
1154
|
+
penalty: {
|
|
1155
|
+
critical: 40,
|
|
1156
|
+
high: 20,
|
|
1157
|
+
moderate: 8,
|
|
1158
|
+
low: 3,
|
|
1159
|
+
info: 0
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
EFFICIENCY = {
|
|
1163
|
+
/** Bundle size at the category median maps to this score; smaller → higher. */
|
|
1164
|
+
medianScore: 50
|
|
1165
|
+
};
|
|
1166
|
+
CONFIDENCE = {
|
|
1167
|
+
proven: {
|
|
1168
|
+
minWeeklyDownloads: 1e5,
|
|
1169
|
+
minAgeMonths: 12,
|
|
1170
|
+
maxLastReleaseMonths: 6
|
|
1171
|
+
},
|
|
1172
|
+
emerging: {
|
|
1173
|
+
minWeeklyDownloads: 5e3,
|
|
1174
|
+
strongGrowth: 0.5,
|
|
1175
|
+
maxLastReleaseMonths: 9
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// src/scoring/score.ts
|
|
1182
|
+
function toScoringInput(signals, category) {
|
|
1183
|
+
const { registry, downloads, github, depsDev, bundle } = signals;
|
|
1184
|
+
return {
|
|
1185
|
+
weeklyDownloads: downloads?.weeklyDownloads ?? null,
|
|
1186
|
+
downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
|
|
1187
|
+
stars: github?.stars ?? null,
|
|
1188
|
+
dependentsCount: null,
|
|
1189
|
+
// not collected in v1 (see spec R1 note)
|
|
1190
|
+
firstPublishedAt: registry?.firstPublishedAt ?? null,
|
|
1191
|
+
lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
|
|
1192
|
+
releasesLast12mo: github?.releasesLast12mo ?? null,
|
|
1193
|
+
openIssues: github?.openIssues ?? null,
|
|
1194
|
+
closedIssues: github?.closedIssues ?? null,
|
|
1195
|
+
scorecard: depsDev?.scorecard ?? null,
|
|
1196
|
+
advisories: depsDev?.advisories ?? [],
|
|
1197
|
+
deprecated: registry?.deprecated ?? false,
|
|
1198
|
+
archived: github?.archived ?? false,
|
|
1199
|
+
bundleMinGzipKb: bundle?.bundleMinGzipKb ?? null,
|
|
1200
|
+
category
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function computeMaintenance(input, now) {
|
|
1204
|
+
if (input.deprecated || input.archived) return MAINTENANCE.deadCap;
|
|
1205
|
+
const components = [];
|
|
1206
|
+
const days = daysSince(input.lastReleaseAt, now);
|
|
1207
|
+
if (days !== null) {
|
|
1208
|
+
const recency = days <= MAINTENANCE.freshDays ? 100 : clamp(100 * (MAINTENANCE.staleDays - days) / (MAINTENANCE.staleDays - MAINTENANCE.freshDays));
|
|
1209
|
+
components.push({ value: recency, weight: MAINTENANCE_WEIGHTS.recency });
|
|
1210
|
+
}
|
|
1211
|
+
if (input.releasesLast12mo !== null) {
|
|
1212
|
+
const cadence = clamp(Math.min(input.releasesLast12mo, MAINTENANCE.cadenceCap) / MAINTENANCE.cadenceCap * 100);
|
|
1213
|
+
components.push({ value: cadence, weight: MAINTENANCE_WEIGHTS.cadence });
|
|
1214
|
+
}
|
|
1215
|
+
if (input.openIssues !== null && input.closedIssues !== null) {
|
|
1216
|
+
const total = input.openIssues + input.closedIssues;
|
|
1217
|
+
if (total > 0) {
|
|
1218
|
+
components.push({ value: input.closedIssues / total * 100, weight: MAINTENANCE_WEIGHTS.closeRatio });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return weightedAverage(components);
|
|
1222
|
+
}
|
|
1223
|
+
function computeAdoption(input) {
|
|
1224
|
+
const dl = input.weeklyDownloads ?? 0;
|
|
1225
|
+
const downloadsScore = clamp(
|
|
1226
|
+
(Math.log10(Math.max(dl, 1)) - ADOPTION.downloadsLogMin) / (ADOPTION.downloadsLogMax - ADOPTION.downloadsLogMin) * 100
|
|
1227
|
+
);
|
|
1228
|
+
let base = downloadsScore;
|
|
1229
|
+
if (input.stars !== null) {
|
|
1230
|
+
const starsScore = clamp(Math.log10(Math.max(input.stars, 1)) / ADOPTION.starsLogMax * 100);
|
|
1231
|
+
base = (1 - ADOPTION.starsWeight) * downloadsScore + ADOPTION.starsWeight * starsScore;
|
|
1232
|
+
}
|
|
1233
|
+
if (input.downloadGrowth90d !== null) {
|
|
1234
|
+
const bonus = clamp(
|
|
1235
|
+
input.downloadGrowth90d * ADOPTION.growthMultiplier,
|
|
1236
|
+
-ADOPTION.growthBonusCap,
|
|
1237
|
+
ADOPTION.growthBonusCap
|
|
1238
|
+
);
|
|
1239
|
+
base += bonus;
|
|
1240
|
+
}
|
|
1241
|
+
return clamp(base);
|
|
1242
|
+
}
|
|
1243
|
+
function computeReliability(input) {
|
|
1244
|
+
const base = input.scorecard !== null ? input.scorecard * 10 : RELIABILITY.neutralWhenUnknown;
|
|
1245
|
+
const penalty = input.advisories.reduce(
|
|
1246
|
+
(sum, adv) => sum + (RELIABILITY.penalty[adv.severity] ?? 0),
|
|
1247
|
+
0
|
|
1248
|
+
);
|
|
1249
|
+
return clamp(base - penalty);
|
|
1250
|
+
}
|
|
1251
|
+
function computeEfficiency(bundleMinGzipKb, category, categoryMedian) {
|
|
1252
|
+
if (!isFrontendCategory(category) || bundleMinGzipKb === null || !categoryMedian || categoryMedian <= 0) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
const ratio = bundleMinGzipKb / categoryMedian;
|
|
1256
|
+
return clamp(100 - ratio * EFFICIENCY.medianScore);
|
|
1257
|
+
}
|
|
1258
|
+
function computeHealthScore(breakdown) {
|
|
1259
|
+
const { maintenance, adoption, reliability, efficiency } = breakdown;
|
|
1260
|
+
if (efficiency === null) {
|
|
1261
|
+
const w = HEALTH_WEIGHTS;
|
|
1262
|
+
const denom = w.maintenance + w.adoption + w.reliability;
|
|
1263
|
+
return Math.round(
|
|
1264
|
+
(w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return Math.round(
|
|
1268
|
+
HEALTH_WEIGHTS.maintenance * maintenance + HEALTH_WEIGHTS.adoption * adoption + HEALTH_WEIGHTS.reliability * reliability + HEALTH_WEIGHTS.efficiency * efficiency
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
function hasCriticalOrHighAdvisory(advisories) {
|
|
1272
|
+
return advisories.some((a) => a.severity === "critical" || a.severity === "high");
|
|
1273
|
+
}
|
|
1274
|
+
function computeConfidence(input, now) {
|
|
1275
|
+
const dl = input.weeklyDownloads ?? 0;
|
|
1276
|
+
const ageMonths = monthsSince(input.firstPublishedAt, now);
|
|
1277
|
+
const lastReleaseMonths = monthsSince(input.lastReleaseAt, now);
|
|
1278
|
+
const growth = input.downloadGrowth90d ?? 0;
|
|
1279
|
+
const provenReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.proven.maxLastReleaseMonths;
|
|
1280
|
+
const provenAgeOk = ageMonths !== null && ageMonths >= CONFIDENCE.proven.minAgeMonths;
|
|
1281
|
+
if (dl >= CONFIDENCE.proven.minWeeklyDownloads && provenAgeOk && provenReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
|
|
1282
|
+
return "proven";
|
|
1283
|
+
}
|
|
1284
|
+
const emergingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.emerging.maxLastReleaseMonths;
|
|
1285
|
+
const emergingAdoptionOk = dl >= CONFIDENCE.emerging.minWeeklyDownloads || growth >= CONFIDENCE.emerging.strongGrowth;
|
|
1286
|
+
if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
|
|
1287
|
+
return "emerging";
|
|
1288
|
+
}
|
|
1289
|
+
return "unproven";
|
|
1290
|
+
}
|
|
1291
|
+
function weightedAverage(components) {
|
|
1292
|
+
if (components.length === 0) return 0;
|
|
1293
|
+
const totalWeight = components.reduce((s, c) => s + c.weight, 0);
|
|
1294
|
+
if (totalWeight === 0) return 0;
|
|
1295
|
+
return Math.round(components.reduce((s, c) => s + c.value * c.weight, 0) / totalWeight);
|
|
1296
|
+
}
|
|
1297
|
+
function median(values) {
|
|
1298
|
+
if (values.length === 0) return null;
|
|
1299
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1300
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1301
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
1302
|
+
}
|
|
1303
|
+
var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince;
|
|
1304
|
+
var init_score = __esm({
|
|
1305
|
+
"src/scoring/score.ts"() {
|
|
1306
|
+
"use strict";
|
|
1307
|
+
init_esm_shims();
|
|
1308
|
+
init_types();
|
|
1309
|
+
init_weights();
|
|
1310
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1311
|
+
MONTH_MS = 30 * DAY_MS;
|
|
1312
|
+
clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
|
|
1313
|
+
daysSince = (date, now) => date ? (now.getTime() - date.getTime()) / DAY_MS : null;
|
|
1314
|
+
monthsSince = (date, now) => date ? (now.getTime() - date.getTime()) / MONTH_MS : null;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// src/scoring/index.ts
|
|
1319
|
+
var init_scoring = __esm({
|
|
1320
|
+
"src/scoring/index.ts"() {
|
|
1321
|
+
"use strict";
|
|
1322
|
+
init_esm_shims();
|
|
1323
|
+
init_score();
|
|
1324
|
+
init_weights();
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// src/search/embeddings.ts
|
|
1329
|
+
import { createHash as createHash2 } from "crypto";
|
|
1330
|
+
function buildEmbeddingText(input) {
|
|
1331
|
+
const body = input.summary || input.description || "";
|
|
1332
|
+
return `${input.name}. ${input.category ?? ""}. ${body}`.trim();
|
|
1333
|
+
}
|
|
1334
|
+
function tokenize(text2) {
|
|
1335
|
+
return text2.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
1336
|
+
}
|
|
1337
|
+
function fnv1a(str2) {
|
|
1338
|
+
let h = 2166136261;
|
|
1339
|
+
for (let i = 0; i < str2.length; i++) {
|
|
1340
|
+
h ^= str2.charCodeAt(i);
|
|
1341
|
+
h = Math.imul(h, 16777619);
|
|
1342
|
+
}
|
|
1343
|
+
return h >>> 0;
|
|
1344
|
+
}
|
|
1345
|
+
function localEmbed(text2, dim2 = EMBEDDING_DIM) {
|
|
1346
|
+
const vec = new Float64Array(dim2);
|
|
1347
|
+
const tokens = tokenize(text2);
|
|
1348
|
+
const add = (token, weight) => {
|
|
1349
|
+
const idx = fnv1a(token) % dim2;
|
|
1350
|
+
const sign = fnv1a("s:" + token) & 1 ? 1 : -1;
|
|
1351
|
+
vec[idx] += weight * sign;
|
|
1352
|
+
};
|
|
1353
|
+
for (const t of tokens) add(t, 1);
|
|
1354
|
+
for (let i = 0; i < tokens.length - 1; i++) add(`${tokens[i]} ${tokens[i + 1]}`, 0.5);
|
|
1355
|
+
let norm = 0;
|
|
1356
|
+
for (let i = 0; i < dim2; i++) norm += vec[i] * vec[i];
|
|
1357
|
+
norm = Math.sqrt(norm) || 1;
|
|
1358
|
+
const out = new Array(dim2);
|
|
1359
|
+
for (let i = 0; i < dim2; i++) out[i] = vec[i] / norm;
|
|
1360
|
+
return out;
|
|
1361
|
+
}
|
|
1362
|
+
function createEmbeddingProvider(fetchImpl) {
|
|
1363
|
+
const config = getConfig();
|
|
1364
|
+
if (config.EMBEDDING_PROVIDER === "openai" && config.EMBEDDING_API_KEY) {
|
|
1365
|
+
return new OpenAIEmbeddingProvider(config.EMBEDDING_API_KEY, config.EMBEDDING_MODEL, fetchImpl);
|
|
1366
|
+
}
|
|
1367
|
+
if (config.EMBEDDING_PROVIDER === "openai" && !config.EMBEDDING_API_KEY) {
|
|
1368
|
+
logger.warn("EMBEDDING_API_KEY not set \u2014 falling back to the local embedder.");
|
|
1369
|
+
}
|
|
1370
|
+
return new LocalEmbeddingProvider();
|
|
1371
|
+
}
|
|
1372
|
+
var LocalEmbeddingProvider, OPENAI_BATCH, OpenAIEmbeddingProvider;
|
|
1373
|
+
var init_embeddings = __esm({
|
|
1374
|
+
"src/search/embeddings.ts"() {
|
|
1375
|
+
"use strict";
|
|
1376
|
+
init_esm_shims();
|
|
1377
|
+
init_config();
|
|
1378
|
+
init_constants();
|
|
1379
|
+
init_http();
|
|
1380
|
+
init_logger();
|
|
1381
|
+
LocalEmbeddingProvider = class {
|
|
1382
|
+
kind = "local";
|
|
1383
|
+
dimensions = EMBEDDING_DIM;
|
|
1384
|
+
async embed(texts) {
|
|
1385
|
+
return texts.map((t) => localEmbed(t));
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
OPENAI_BATCH = 96;
|
|
1389
|
+
OpenAIEmbeddingProvider = class {
|
|
1390
|
+
constructor(apiKey, model, fetchImpl) {
|
|
1391
|
+
this.apiKey = apiKey;
|
|
1392
|
+
this.model = model;
|
|
1393
|
+
this.fetchImpl = fetchImpl;
|
|
1394
|
+
}
|
|
1395
|
+
apiKey;
|
|
1396
|
+
model;
|
|
1397
|
+
fetchImpl;
|
|
1398
|
+
kind = "openai";
|
|
1399
|
+
dimensions = EMBEDDING_DIM;
|
|
1400
|
+
async embed(texts) {
|
|
1401
|
+
const out = [];
|
|
1402
|
+
for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
|
|
1403
|
+
const batch = texts.slice(i, i + OPENAI_BATCH);
|
|
1404
|
+
const body = JSON.stringify({ model: this.model, input: batch });
|
|
1405
|
+
const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
|
|
1406
|
+
host: "api.openai.com",
|
|
1407
|
+
method: "POST",
|
|
1408
|
+
ttlMs: 30 * 24 * 60 * 60 * 1e3,
|
|
1409
|
+
// cache embeddings 30d to control cost
|
|
1410
|
+
cacheKey: `openai-embed ${this.model} ${createHash2("sha256").update(body).digest("hex")}`,
|
|
1411
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
|
|
1412
|
+
body,
|
|
1413
|
+
fetchImpl: this.fetchImpl
|
|
1414
|
+
});
|
|
1415
|
+
const vectors = data?.data ?? [];
|
|
1416
|
+
vectors.sort((a, b) => a.index - b.index);
|
|
1417
|
+
for (const v of vectors) out.push(v.embedding);
|
|
1418
|
+
}
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// src/db/packages.ts
|
|
1426
|
+
import { eq, sql } from "drizzle-orm";
|
|
1427
|
+
async function getSeedTargets(db) {
|
|
1428
|
+
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1429
|
+
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
1430
|
+
}
|
|
1431
|
+
async function getPackageByName(db, name) {
|
|
1432
|
+
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
1433
|
+
return rows[0] ?? null;
|
|
1434
|
+
}
|
|
1435
|
+
async function ensureSeedEntry(db, name, category) {
|
|
1436
|
+
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1437
|
+
}
|
|
1438
|
+
async function upsertPackage(db, row) {
|
|
1439
|
+
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
1440
|
+
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1441
|
+
target: packages.name,
|
|
1442
|
+
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
async function startSyncRun(db) {
|
|
1446
|
+
const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
|
|
1447
|
+
return row.id;
|
|
1448
|
+
}
|
|
1449
|
+
async function finishSyncRun(db, id, data) {
|
|
1450
|
+
await db.update(syncRuns).set({
|
|
1451
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1452
|
+
packagesSeen: data.packagesSeen,
|
|
1453
|
+
packagesUpdated: data.packagesUpdated,
|
|
1454
|
+
errors: data.errors,
|
|
1455
|
+
status: data.status
|
|
1456
|
+
}).where(eq(syncRuns.id, id));
|
|
1457
|
+
}
|
|
1458
|
+
var init_packages = __esm({
|
|
1459
|
+
"src/db/packages.ts"() {
|
|
1460
|
+
"use strict";
|
|
1461
|
+
init_esm_shims();
|
|
1462
|
+
init_schema();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// src/core/concurrency.ts
|
|
1467
|
+
async function pMap(items, mapper, concurrency) {
|
|
1468
|
+
const results = new Array(items.length);
|
|
1469
|
+
let cursor = 0;
|
|
1470
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
1471
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1472
|
+
while (cursor < items.length) {
|
|
1473
|
+
const index2 = cursor++;
|
|
1474
|
+
results[index2] = await mapper(items[index2], index2);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
await Promise.all(workers);
|
|
1478
|
+
return results;
|
|
1479
|
+
}
|
|
1480
|
+
var init_concurrency = __esm({
|
|
1481
|
+
"src/core/concurrency.ts"() {
|
|
1482
|
+
"use strict";
|
|
1483
|
+
init_esm_shims();
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
// src/pipeline/sync.ts
|
|
1488
|
+
async function runSync(opts = {}) {
|
|
1489
|
+
const config = getConfig();
|
|
1490
|
+
const now = /* @__PURE__ */ new Date();
|
|
1491
|
+
if (opts.full) setCacheBypassRead(true);
|
|
1492
|
+
const handle = createDb({ max: Math.max(4, config.LURQ_SYNC_CONCURRENCY) });
|
|
1493
|
+
const provider = createSummaryProvider();
|
|
1494
|
+
logger.info(`Summary provider: ${provider.kind}`);
|
|
1495
|
+
const runId = await startSyncRun(handle.db);
|
|
1496
|
+
const allErrors = [];
|
|
1497
|
+
try {
|
|
1498
|
+
const targets = await resolveTargets(handle.db, opts);
|
|
1499
|
+
logger.info(`Syncing ${targets.length} package(s) with concurrency ${config.LURQ_SYNC_CONCURRENCY}\u2026`);
|
|
1500
|
+
logger.info("Fetching weekly downloads in bulk\u2026");
|
|
1501
|
+
const weeklyMap = await fetchBulkWeeklyDownloads(targets.map((t) => t.name));
|
|
1502
|
+
let done = 0;
|
|
1503
|
+
const computed = await pMap(
|
|
1504
|
+
targets,
|
|
1505
|
+
async (target) => {
|
|
1506
|
+
try {
|
|
1507
|
+
const signals = await collectSignals(target.name, target.category, {
|
|
1508
|
+
githubToken: config.GITHUB_TOKEN,
|
|
1509
|
+
prefetchedWeekly: weeklyMap.has(target.name) ? weeklyMap.get(target.name) : void 0
|
|
1510
|
+
});
|
|
1511
|
+
for (const e of signals.errors) allErrors.push({ package: target.name, ...e });
|
|
1512
|
+
const input = toScoringInput(signals, target.category);
|
|
1513
|
+
const summaryInput = await buildSummaryInput(signals, target.category);
|
|
1514
|
+
const { summary, usageGuide } = await provider.generate(summaryInput);
|
|
1515
|
+
if (++done % 25 === 0) logger.info(` \u2026${done}/${targets.length}`);
|
|
1516
|
+
return {
|
|
1517
|
+
target,
|
|
1518
|
+
signals,
|
|
1519
|
+
input,
|
|
1520
|
+
maintenance: computeMaintenance(input, now),
|
|
1521
|
+
adoption: computeAdoption(input),
|
|
1522
|
+
reliability: computeReliability(input),
|
|
1523
|
+
confidence: computeConfidence(input, now),
|
|
1524
|
+
summary,
|
|
1525
|
+
usageGuide
|
|
1526
|
+
};
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
allErrors.push({
|
|
1529
|
+
package: target.name,
|
|
1530
|
+
source: "pipeline",
|
|
1531
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1532
|
+
});
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
},
|
|
1536
|
+
config.LURQ_SYNC_CONCURRENCY
|
|
1537
|
+
);
|
|
1538
|
+
const ok = computed.filter((c) => c !== null);
|
|
1539
|
+
const medians = computeCategoryMedians(ok);
|
|
1540
|
+
const embProvider = createEmbeddingProvider();
|
|
1541
|
+
logger.info(`Embedding provider: ${embProvider.kind}`);
|
|
1542
|
+
const embeddings = await embProvider.embed(
|
|
1543
|
+
ok.map(
|
|
1544
|
+
(c) => buildEmbeddingText({
|
|
1545
|
+
name: c.target.name,
|
|
1546
|
+
category: c.target.category,
|
|
1547
|
+
summary: c.summary,
|
|
1548
|
+
description: c.signals.registry?.description ?? null
|
|
1549
|
+
})
|
|
1550
|
+
)
|
|
1551
|
+
);
|
|
1552
|
+
let updated = 0;
|
|
1553
|
+
for (let i = 0; i < ok.length; i++) {
|
|
1554
|
+
const c = ok[i];
|
|
1555
|
+
const efficiency = computeEfficiency(
|
|
1556
|
+
c.input.bundleMinGzipKb,
|
|
1557
|
+
c.target.category,
|
|
1558
|
+
c.target.category ? medians.get(c.target.category) ?? null : null
|
|
1559
|
+
);
|
|
1560
|
+
const breakdown = {
|
|
1561
|
+
maintenance: c.maintenance,
|
|
1562
|
+
adoption: c.adoption,
|
|
1563
|
+
reliability: c.reliability,
|
|
1564
|
+
efficiency
|
|
1565
|
+
};
|
|
1566
|
+
const healthScore = computeHealthScore(breakdown);
|
|
1567
|
+
await upsertPackage(
|
|
1568
|
+
handle.db,
|
|
1569
|
+
assemblePackageRow({
|
|
1570
|
+
name: c.target.name,
|
|
1571
|
+
category: c.target.category,
|
|
1572
|
+
signals: c.signals,
|
|
1573
|
+
input: c.input,
|
|
1574
|
+
summary: c.summary,
|
|
1575
|
+
usageGuide: c.usageGuide,
|
|
1576
|
+
confidence: c.confidence,
|
|
1577
|
+
breakdown,
|
|
1578
|
+
healthScore,
|
|
1579
|
+
embedding: embeddings[i] ?? null,
|
|
1580
|
+
now
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
updated++;
|
|
1584
|
+
}
|
|
1585
|
+
const status = updated === 0 ? "failed" : allErrors.length > 0 ? "partial" : "success";
|
|
1586
|
+
await finishSyncRun(handle.db, runId, {
|
|
1587
|
+
packagesSeen: targets.length,
|
|
1588
|
+
packagesUpdated: updated,
|
|
1589
|
+
errors: allErrors,
|
|
1590
|
+
status
|
|
1591
|
+
});
|
|
1592
|
+
logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
|
|
1593
|
+
return { seen: targets.length, updated, errors: allErrors.length, status };
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
await finishSyncRun(handle.db, runId, {
|
|
1596
|
+
packagesSeen: 0,
|
|
1597
|
+
packagesUpdated: 0,
|
|
1598
|
+
errors: [{ package: "*", source: "pipeline", message: err.message }],
|
|
1599
|
+
status: "failed"
|
|
1600
|
+
});
|
|
1601
|
+
throw err;
|
|
1602
|
+
} finally {
|
|
1603
|
+
if (opts.full) setCacheBypassRead(false);
|
|
1604
|
+
await handle.close();
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async function resolveTargets(db, opts) {
|
|
1608
|
+
if (opts.packageName) {
|
|
1609
|
+
const seeds = await getSeedTargets(db);
|
|
1610
|
+
const found = seeds.find((s) => s.name === opts.packageName);
|
|
1611
|
+
return [{ name: opts.packageName, category: found?.category ?? null }];
|
|
1612
|
+
}
|
|
1613
|
+
return getSeedTargets(db);
|
|
1614
|
+
}
|
|
1615
|
+
function computeCategoryMedians(computed) {
|
|
1616
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1617
|
+
for (const c of computed) {
|
|
1618
|
+
const cat = c.target.category;
|
|
1619
|
+
const kb = c.input.bundleMinGzipKb;
|
|
1620
|
+
if (cat && isFrontendCategory(cat) && kb !== null) {
|
|
1621
|
+
const list = byCategory.get(cat) ?? [];
|
|
1622
|
+
list.push(kb);
|
|
1623
|
+
byCategory.set(cat, list);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const medians = /* @__PURE__ */ new Map();
|
|
1627
|
+
for (const [cat, list] of byCategory) {
|
|
1628
|
+
const m = median(list);
|
|
1629
|
+
if (m !== null) medians.set(cat, m);
|
|
1630
|
+
}
|
|
1631
|
+
return medians;
|
|
1632
|
+
}
|
|
1633
|
+
function assemblePackageRow(p) {
|
|
1634
|
+
const r = p.signals.registry;
|
|
1635
|
+
return {
|
|
1636
|
+
name: p.name,
|
|
1637
|
+
ecosystem: "npm",
|
|
1638
|
+
category: p.category,
|
|
1639
|
+
description: r?.description ?? null,
|
|
1640
|
+
summary: p.summary,
|
|
1641
|
+
repoUrl: r?.repoUrl ?? null,
|
|
1642
|
+
homepage: r?.homepage ?? null,
|
|
1643
|
+
latestVersion: r?.latestVersion ?? null,
|
|
1644
|
+
license: r?.license ?? null,
|
|
1645
|
+
deprecated: p.input.deprecated,
|
|
1646
|
+
archived: p.input.archived,
|
|
1647
|
+
firstPublishedAt: p.input.firstPublishedAt,
|
|
1648
|
+
lastReleaseAt: p.input.lastReleaseAt,
|
|
1649
|
+
weeklyDownloads: p.input.weeklyDownloads,
|
|
1650
|
+
downloadGrowth90d: p.input.downloadGrowth90d,
|
|
1651
|
+
dependentsCount: p.input.dependentsCount,
|
|
1652
|
+
stars: p.input.stars,
|
|
1653
|
+
openIssues: p.input.openIssues,
|
|
1654
|
+
closedIssues: p.input.closedIssues,
|
|
1655
|
+
scorecard: p.input.scorecard,
|
|
1656
|
+
bundleMinGzipKb: p.input.bundleMinGzipKb,
|
|
1657
|
+
advisories: p.input.advisories,
|
|
1658
|
+
healthScore: p.healthScore,
|
|
1659
|
+
confidence: p.confidence,
|
|
1660
|
+
scoreBreakdown: p.breakdown,
|
|
1661
|
+
usageGuide: p.usageGuide,
|
|
1662
|
+
embedding: p.embedding,
|
|
1663
|
+
dataAsOf: p.now
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
var init_sync = __esm({
|
|
1667
|
+
"src/pipeline/sync.ts"() {
|
|
1668
|
+
"use strict";
|
|
1669
|
+
init_esm_shims();
|
|
1670
|
+
init_config();
|
|
1671
|
+
init_concurrency();
|
|
1672
|
+
init_http();
|
|
1673
|
+
init_logger();
|
|
1674
|
+
init_collect();
|
|
1675
|
+
init_npmDownloads();
|
|
1676
|
+
init_summarize();
|
|
1677
|
+
init_embeddings();
|
|
1678
|
+
init_scoring();
|
|
1679
|
+
init_client();
|
|
1680
|
+
init_packages();
|
|
1681
|
+
init_types();
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
// src/pipeline/single.ts
|
|
1686
|
+
import { and, eq as eq2, isNotNull, sql as sql2 } from "drizzle-orm";
|
|
1687
|
+
async function getSeedCategory(db, name) {
|
|
1688
|
+
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
|
|
1689
|
+
return row?.category ?? null;
|
|
1690
|
+
}
|
|
1691
|
+
async function getCategoryMedianBundle(db, category) {
|
|
1692
|
+
const [row] = await db.select({
|
|
1693
|
+
m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
1694
|
+
}).from(packages).where(and(eq2(packages.category, category), isNotNull(packages.bundleMinGzipKb)));
|
|
1695
|
+
return row?.m ?? null;
|
|
1696
|
+
}
|
|
1697
|
+
async function syncOnePackage(db, name, opts = {}) {
|
|
1698
|
+
const config = getConfig();
|
|
1699
|
+
const now = /* @__PURE__ */ new Date();
|
|
1700
|
+
const existing = await getPackageByName(db, name);
|
|
1701
|
+
const category = opts.category ?? existing?.category ?? await getSeedCategory(db, name) ?? null;
|
|
1702
|
+
const prefetchedWeekly = await fetchWeeklyDownloads(name).catch(() => null);
|
|
1703
|
+
const signals = await collectSignals(name, category, {
|
|
1704
|
+
githubToken: config.GITHUB_TOKEN,
|
|
1705
|
+
prefetchedWeekly
|
|
1706
|
+
});
|
|
1707
|
+
const input = toScoringInput(signals, category);
|
|
1708
|
+
const summaryInput = await buildSummaryInput(signals, category);
|
|
1709
|
+
const { summary, usageGuide } = await createSummaryProvider().generate(summaryInput);
|
|
1710
|
+
const median2 = category ? await getCategoryMedianBundle(db, category) : null;
|
|
1711
|
+
const breakdown = {
|
|
1712
|
+
maintenance: computeMaintenance(input, now),
|
|
1713
|
+
adoption: computeAdoption(input),
|
|
1714
|
+
reliability: computeReliability(input),
|
|
1715
|
+
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2)
|
|
1716
|
+
};
|
|
1717
|
+
const healthScore = computeHealthScore(breakdown);
|
|
1718
|
+
const confidence = computeConfidence(input, now);
|
|
1719
|
+
const [embedding] = await createEmbeddingProvider().embed([
|
|
1720
|
+
buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
|
|
1721
|
+
]);
|
|
1722
|
+
await upsertPackage(
|
|
1723
|
+
db,
|
|
1724
|
+
assemblePackageRow({
|
|
1725
|
+
name,
|
|
1726
|
+
category,
|
|
1727
|
+
signals,
|
|
1728
|
+
input,
|
|
1729
|
+
summary,
|
|
1730
|
+
usageGuide,
|
|
1731
|
+
confidence,
|
|
1732
|
+
breakdown,
|
|
1733
|
+
healthScore,
|
|
1734
|
+
embedding: embedding ?? null,
|
|
1735
|
+
now
|
|
1736
|
+
})
|
|
1737
|
+
);
|
|
1738
|
+
return await getPackageByName(db, name);
|
|
1739
|
+
}
|
|
1740
|
+
async function getOrFetchPackage(db, name) {
|
|
1741
|
+
const existing = await getPackageByName(db, name);
|
|
1742
|
+
if (existing) return { row: existing, wasTracked: true, existsOnNpm: true };
|
|
1743
|
+
const exists = await npmPackageExists(name);
|
|
1744
|
+
if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
|
|
1745
|
+
const row = await syncOnePackage(db, name);
|
|
1746
|
+
await ensureSeedEntry(db, name, row.category);
|
|
1747
|
+
return { row, wasTracked: false, existsOnNpm: true };
|
|
1748
|
+
}
|
|
1749
|
+
var init_single = __esm({
|
|
1750
|
+
"src/pipeline/single.ts"() {
|
|
1751
|
+
"use strict";
|
|
1752
|
+
init_esm_shims();
|
|
1753
|
+
init_config();
|
|
1754
|
+
init_collect();
|
|
1755
|
+
init_sources();
|
|
1756
|
+
init_summarize();
|
|
1757
|
+
init_scoring();
|
|
1758
|
+
init_embeddings();
|
|
1759
|
+
init_packages();
|
|
1760
|
+
init_schema();
|
|
1761
|
+
init_sync();
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// src/search/categoryInference.ts
|
|
1766
|
+
function inferCategory(need) {
|
|
1767
|
+
const text2 = need.toLowerCase();
|
|
1768
|
+
for (const rule of RULES) {
|
|
1769
|
+
if (rule.patterns.test(text2)) return rule.category;
|
|
1770
|
+
}
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
var RULES;
|
|
1774
|
+
var init_categoryInference = __esm({
|
|
1775
|
+
"src/search/categoryInference.ts"() {
|
|
1776
|
+
"use strict";
|
|
1777
|
+
init_esm_shims();
|
|
1778
|
+
RULES = [
|
|
1779
|
+
{ category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
|
|
1780
|
+
{ category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
|
|
1781
|
+
{ category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
|
|
1782
|
+
{ category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
|
|
1783
|
+
{ category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
|
|
1784
|
+
{ category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
|
|
1785
|
+
{ category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
|
|
1786
|
+
{ category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
|
|
1787
|
+
{ category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
|
|
1788
|
+
{ category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
|
|
1789
|
+
{ category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
|
|
1790
|
+
{ category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
|
|
1791
|
+
{ category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
|
|
1792
|
+
{ category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
|
|
1793
|
+
{ category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
|
|
1794
|
+
// date-time precedes linting: "date formatting" must not be captured by linting's
|
|
1795
|
+
// bare "formatting" token before the date rule is reached (first-match-wins).
|
|
1796
|
+
{ category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
|
|
1797
|
+
{ category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
|
|
1798
|
+
{ category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
|
|
1799
|
+
{ category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
|
|
1800
|
+
{ category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
|
|
1801
|
+
{ category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
|
|
1802
|
+
{ category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
|
|
1803
|
+
];
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
// src/search/recommend.ts
|
|
1808
|
+
import { and as and2, cosineDistance, desc, eq as eq3, isNotNull as isNotNull2, lte, sql as sql3 } from "drizzle-orm";
|
|
1809
|
+
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
1810
|
+
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
1811
|
+
const [queryVec] = await provider.embed([opts.need]);
|
|
1812
|
+
if (!queryVec) return [];
|
|
1813
|
+
const category = opts.category ?? inferCategory(opts.need);
|
|
1814
|
+
const pool = Math.max(limit * 5, 25);
|
|
1815
|
+
let rows = await runQuery(db, queryVec, opts.constraints, category, pool);
|
|
1816
|
+
if (category && rows.length < limit) {
|
|
1817
|
+
const broad = await runQuery(db, queryVec, opts.constraints, null, pool);
|
|
1818
|
+
const seen = new Set(rows.map((r) => r.name));
|
|
1819
|
+
rows = rows.concat(broad.filter((r) => !seen.has(r.name)));
|
|
1820
|
+
}
|
|
1821
|
+
if (rows.length === 0) return [];
|
|
1822
|
+
const ranked = rows.map((r) => {
|
|
1823
|
+
const simNorm = Math.max(0, Math.min(1, (r.similarity + 1) / 2));
|
|
1824
|
+
const health = (r.healthScore ?? 0) / 100;
|
|
1825
|
+
return { row: r, score: SIM_WEIGHT * simNorm + HEALTH_WEIGHT * health };
|
|
1826
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1827
|
+
return ranked.map(({ row }) => toCandidate(row));
|
|
1828
|
+
}
|
|
1829
|
+
async function runQuery(db, queryVec, constraints, category, pool) {
|
|
1830
|
+
const similarity = sql3`1 - (${cosineDistance(packages.embedding, queryVec)})`;
|
|
1831
|
+
const conditions = [isNotNull2(packages.embedding)];
|
|
1832
|
+
if (category) conditions.push(eq3(packages.category, category));
|
|
1833
|
+
if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
|
|
1834
|
+
if (constraints?.maxBundleKb !== void 0) {
|
|
1835
|
+
conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
|
|
1836
|
+
}
|
|
1837
|
+
if (constraints?.minConfidence) {
|
|
1838
|
+
const allowed = ["proven", "emerging", "unproven"].filter(
|
|
1839
|
+
(c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
|
|
1840
|
+
);
|
|
1841
|
+
conditions.push(
|
|
1842
|
+
sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
return db.select({
|
|
1846
|
+
name: packages.name,
|
|
1847
|
+
category: packages.category,
|
|
1848
|
+
healthScore: packages.healthScore,
|
|
1849
|
+
confidence: packages.confidence,
|
|
1850
|
+
latestVersion: packages.latestVersion,
|
|
1851
|
+
weeklyDownloads: packages.weeklyDownloads,
|
|
1852
|
+
lastReleaseAt: packages.lastReleaseAt,
|
|
1853
|
+
repoUrl: packages.repoUrl,
|
|
1854
|
+
similarity
|
|
1855
|
+
}).from(packages).where(and2(...conditions)).orderBy(desc(similarity)).limit(pool);
|
|
1856
|
+
}
|
|
1857
|
+
function toCandidate(row) {
|
|
1858
|
+
return {
|
|
1859
|
+
name: row.name,
|
|
1860
|
+
category: row.category,
|
|
1861
|
+
healthScore: row.healthScore ?? 0,
|
|
1862
|
+
confidence: row.confidence ?? "unproven",
|
|
1863
|
+
why: buildWhy(row),
|
|
1864
|
+
latestVersion: row.latestVersion,
|
|
1865
|
+
weeklyDownloads: row.weeklyDownloads,
|
|
1866
|
+
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
1867
|
+
repoUrl: row.repoUrl
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function buildWhy(row) {
|
|
1871
|
+
const parts = [];
|
|
1872
|
+
if (row.confidence) parts.push(row.confidence);
|
|
1873
|
+
if (row.weeklyDownloads) parts.push(`${formatDownloads(row.weeklyDownloads)} weekly downloads`);
|
|
1874
|
+
if (row.healthScore !== null) parts.push(`health ${row.healthScore}`);
|
|
1875
|
+
return parts.join(", ") || "tracked package";
|
|
1876
|
+
}
|
|
1877
|
+
function formatDownloads(n) {
|
|
1878
|
+
if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
|
|
1879
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
1880
|
+
return String(n);
|
|
1881
|
+
}
|
|
1882
|
+
var CONFIDENCE_RANK, SIM_WEIGHT, HEALTH_WEIGHT;
|
|
1883
|
+
var init_recommend = __esm({
|
|
1884
|
+
"src/search/recommend.ts"() {
|
|
1885
|
+
"use strict";
|
|
1886
|
+
init_esm_shims();
|
|
1887
|
+
init_schema();
|
|
1888
|
+
init_categoryInference();
|
|
1889
|
+
init_embeddings();
|
|
1890
|
+
CONFIDENCE_RANK = { unproven: 0, emerging: 1, proven: 2 };
|
|
1891
|
+
SIM_WEIGHT = 0.6;
|
|
1892
|
+
HEALTH_WEIGHT = 0.4;
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// src/mcp/handlers.ts
|
|
1897
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
1898
|
+
function isStale(dataAsOf) {
|
|
1899
|
+
if (!dataAsOf) return true;
|
|
1900
|
+
return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
|
|
1901
|
+
}
|
|
1902
|
+
function withinDays(date, days) {
|
|
1903
|
+
return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
|
|
1904
|
+
}
|
|
1905
|
+
function topAdvisories(advisories, max = 5) {
|
|
1906
|
+
if (!advisories?.length) return [];
|
|
1907
|
+
return [...advisories].sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).slice(0, max);
|
|
1908
|
+
}
|
|
1909
|
+
function rowToEvaluate(row) {
|
|
1910
|
+
const breakdown = row.scoreBreakdown ?? {
|
|
1911
|
+
maintenance: 0,
|
|
1912
|
+
adoption: 0,
|
|
1913
|
+
reliability: 0,
|
|
1914
|
+
efficiency: null
|
|
1915
|
+
};
|
|
1916
|
+
return {
|
|
1917
|
+
dataAsOf: (row.dataAsOf ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
1918
|
+
stale: isStale(row.dataAsOf) || void 0,
|
|
1919
|
+
name: row.name,
|
|
1920
|
+
category: row.category,
|
|
1921
|
+
healthScore: row.healthScore ?? 0,
|
|
1922
|
+
confidence: row.confidence ?? "unproven",
|
|
1923
|
+
scoreBreakdown: breakdown,
|
|
1924
|
+
latestVersion: row.latestVersion,
|
|
1925
|
+
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
1926
|
+
weeklyDownloads: row.weeklyDownloads,
|
|
1927
|
+
downloadGrowth90d: row.downloadGrowth90d,
|
|
1928
|
+
dependentsCount: row.dependentsCount,
|
|
1929
|
+
scorecard: row.scorecard,
|
|
1930
|
+
bundleMinGzipKb: row.bundleMinGzipKb,
|
|
1931
|
+
deprecated: row.deprecated,
|
|
1932
|
+
archived: row.archived,
|
|
1933
|
+
advisories: topAdvisories(row.advisories),
|
|
1934
|
+
summary: row.summary ? truncateSentences(row.summary, 3) : null,
|
|
1935
|
+
usageGuide: row.usageGuide ?? null,
|
|
1936
|
+
repoUrl: row.repoUrl
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
async function latestDataAsOf(db) {
|
|
1940
|
+
const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
|
|
1941
|
+
return new Date(row?.m ?? Date.now()).toISOString();
|
|
1942
|
+
}
|
|
1943
|
+
async function handleRecommend(db, input) {
|
|
1944
|
+
const candidates = await recommend(db, {
|
|
1945
|
+
need: input.need,
|
|
1946
|
+
category: input.category,
|
|
1947
|
+
constraints: input.constraints,
|
|
1948
|
+
limit: 5
|
|
1949
|
+
});
|
|
1950
|
+
return { dataAsOf: await latestDataAsOf(db), candidates };
|
|
1951
|
+
}
|
|
1952
|
+
async function handleEvaluate(db, input) {
|
|
1953
|
+
const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
|
|
1954
|
+
if (!row) {
|
|
1955
|
+
return {
|
|
1956
|
+
tracked: false,
|
|
1957
|
+
suggestion: existsOnNpm ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
return rowToEvaluate(row);
|
|
1961
|
+
}
|
|
1962
|
+
async function handleCompare(db, input) {
|
|
1963
|
+
const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
|
|
1964
|
+
const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
|
|
1965
|
+
const missing = input.packages.filter(
|
|
1966
|
+
(name) => !rows.some((r) => r.name === name)
|
|
1967
|
+
);
|
|
1968
|
+
return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
|
|
1969
|
+
}
|
|
1970
|
+
async function handleVerify(db, input) {
|
|
1971
|
+
const name = input.package;
|
|
1972
|
+
const exists = await npmPackageExists(name);
|
|
1973
|
+
if (!exists) {
|
|
1974
|
+
return {
|
|
1975
|
+
exists: false,
|
|
1976
|
+
tracked: false,
|
|
1977
|
+
deprecated: false,
|
|
1978
|
+
archived: false,
|
|
1979
|
+
latestVersion: null,
|
|
1980
|
+
weeklyDownloads: null,
|
|
1981
|
+
riskFlags: ["not-found-on-registry"],
|
|
1982
|
+
confidence: null,
|
|
1983
|
+
advisoryCount: 0
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
const registry = await fetchNpmRegistry(name).catch(() => null);
|
|
1987
|
+
const { row, wasTracked } = await getOrFetchPackage(db, name);
|
|
1988
|
+
const weeklyDownloads = row?.weeklyDownloads ?? null;
|
|
1989
|
+
const advisoryCount = row?.advisories?.length ?? 0;
|
|
1990
|
+
const deprecated = Boolean(row?.deprecated || registry?.deprecated);
|
|
1991
|
+
const archived = Boolean(row?.archived);
|
|
1992
|
+
const riskFlags = [];
|
|
1993
|
+
if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
|
|
1994
|
+
else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
|
|
1995
|
+
if (withinDays(registry?.firstPublishedAt ?? null, 7)) riskFlags.push("published-within-7-days");
|
|
1996
|
+
if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
|
|
1997
|
+
if (advisoryCount > 0) riskFlags.push("has-known-advisory");
|
|
1998
|
+
if (deprecated) riskFlags.push("deprecated");
|
|
1999
|
+
if (archived) riskFlags.push("archived");
|
|
2000
|
+
return {
|
|
2001
|
+
exists: true,
|
|
2002
|
+
tracked: wasTracked,
|
|
2003
|
+
deprecated,
|
|
2004
|
+
archived,
|
|
2005
|
+
latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
|
|
2006
|
+
weeklyDownloads,
|
|
2007
|
+
riskFlags,
|
|
2008
|
+
confidence: row?.confidence ?? null,
|
|
2009
|
+
advisoryCount
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
var SEVERITY_RANK, DAY_MS2;
|
|
2013
|
+
var init_handlers = __esm({
|
|
2014
|
+
"src/mcp/handlers.ts"() {
|
|
2015
|
+
"use strict";
|
|
2016
|
+
init_esm_shims();
|
|
2017
|
+
init_constants();
|
|
2018
|
+
init_schema();
|
|
2019
|
+
init_sources();
|
|
2020
|
+
init_summarize();
|
|
2021
|
+
init_single();
|
|
2022
|
+
init_recommend();
|
|
2023
|
+
SEVERITY_RANK = {
|
|
2024
|
+
critical: 4,
|
|
2025
|
+
high: 3,
|
|
2026
|
+
moderate: 2,
|
|
2027
|
+
low: 1,
|
|
2028
|
+
info: 0
|
|
2029
|
+
};
|
|
2030
|
+
DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
// src/mcp/diagram.ts
|
|
2035
|
+
import { inArray } from "drizzle-orm";
|
|
2036
|
+
function layerFor(item) {
|
|
2037
|
+
if (!item.category) return UNCLASSIFIED;
|
|
2038
|
+
if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
|
|
2039
|
+
return LAYER_OF[item.category] ?? UNCLASSIFIED;
|
|
2040
|
+
}
|
|
2041
|
+
function nodeId(label, i) {
|
|
2042
|
+
return `n${i}_${label.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
2043
|
+
}
|
|
2044
|
+
function buildMermaid(items) {
|
|
2045
|
+
const byLayer = /* @__PURE__ */ new Map();
|
|
2046
|
+
for (const item of items) {
|
|
2047
|
+
const layer = layerFor(item);
|
|
2048
|
+
const list = byLayer.get(layer) ?? [];
|
|
2049
|
+
list.push(item);
|
|
2050
|
+
byLayer.set(layer, list);
|
|
2051
|
+
}
|
|
2052
|
+
const lines = ["flowchart TD", " user([User])"];
|
|
2053
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2054
|
+
let counter = 0;
|
|
2055
|
+
const repNode = /* @__PURE__ */ new Map();
|
|
2056
|
+
const emitLayer = (layer) => {
|
|
2057
|
+
const list = byLayer.get(layer);
|
|
2058
|
+
if (!list?.length) return;
|
|
2059
|
+
lines.push(` subgraph ${layer.replace(/\s+/g, "_")}["${layer}"]`);
|
|
2060
|
+
for (const item of list) {
|
|
2061
|
+
const id = nodeId(item.label, counter++);
|
|
2062
|
+
ids.set(item.label, id);
|
|
2063
|
+
if (!repNode.has(layer)) repNode.set(layer, id);
|
|
2064
|
+
const text2 = layer === UNCLASSIFIED ? item.label : `${item.category}: ${item.label}`;
|
|
2065
|
+
lines.push(` ${id}["${text2}"]`);
|
|
2066
|
+
}
|
|
2067
|
+
lines.push(" end");
|
|
2068
|
+
};
|
|
2069
|
+
for (const layer of FLOW_LAYERS) emitLayer(layer);
|
|
2070
|
+
emitLayer("Tooling");
|
|
2071
|
+
emitLayer(UNCLASSIFIED);
|
|
2072
|
+
const present = FLOW_LAYERS.filter((l) => byLayer.has(l));
|
|
2073
|
+
let prev = "user";
|
|
2074
|
+
for (const layer of present) {
|
|
2075
|
+
lines.push(` ${prev} --> ${repNode.get(layer)}`);
|
|
2076
|
+
prev = repNode.get(layer);
|
|
2077
|
+
}
|
|
2078
|
+
if (byLayer.has("Data")) lines.push(` ${repNode.get("Data")} --> db[(Database)]`);
|
|
2079
|
+
return lines.join("\n");
|
|
2080
|
+
}
|
|
2081
|
+
async function handleDiagram(db, input) {
|
|
2082
|
+
if (!input.stack?.length) {
|
|
2083
|
+
return {
|
|
2084
|
+
mermaid: "flowchart TD\n user([User]) --> app[Application]",
|
|
2085
|
+
note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray(packages.name, input.stack));
|
|
2089
|
+
const known = new Map(rows.map((r) => [r.name, r.category]));
|
|
2090
|
+
const items = input.stack.map((name) => ({
|
|
2091
|
+
label: name,
|
|
2092
|
+
category: known.get(name) ?? inferCategory(name)
|
|
2093
|
+
}));
|
|
2094
|
+
const unclassified = items.filter((i) => layerFor(i) === UNCLASSIFIED).map((i) => i.label);
|
|
2095
|
+
const note = unclassified.length ? `Reference pattern derived from the provided stack \u2014 partial, not authoritative. Could not place ${unclassified.length} package(s): ${unclassified.join(", ")} (not in lurq's index and no category match) \u2014 shown under Unclassified, outside the flow.` : "Reference pattern derived from the provided stack. A labeled starting point, not a validated architecture.";
|
|
2096
|
+
return { mermaid: buildMermaid(items), note };
|
|
2097
|
+
}
|
|
2098
|
+
var UNCLASSIFIED, LAYER_OF, FLOW_LAYERS, BACKEND_FRAMEWORKS;
|
|
2099
|
+
var init_diagram = __esm({
|
|
2100
|
+
"src/mcp/diagram.ts"() {
|
|
2101
|
+
"use strict";
|
|
2102
|
+
init_esm_shims();
|
|
2103
|
+
init_categoryInference();
|
|
2104
|
+
init_schema();
|
|
2105
|
+
UNCLASSIFIED = "Unclassified";
|
|
2106
|
+
LAYER_OF = {
|
|
2107
|
+
framework: "Presentation",
|
|
2108
|
+
"meta-framework": "Presentation",
|
|
2109
|
+
"ui-component-library": "Presentation",
|
|
2110
|
+
styling: "Presentation",
|
|
2111
|
+
animation: "Presentation",
|
|
2112
|
+
charts: "Presentation",
|
|
2113
|
+
"state-management": "Client Logic",
|
|
2114
|
+
routing: "Client Logic",
|
|
2115
|
+
forms: "Client Logic",
|
|
2116
|
+
validation: "Client Logic",
|
|
2117
|
+
"data-fetching": "Client Logic",
|
|
2118
|
+
"http-client": "Client Logic",
|
|
2119
|
+
i18n: "Client Logic",
|
|
2120
|
+
"date-time": "Client Logic",
|
|
2121
|
+
utility: "Client Logic",
|
|
2122
|
+
auth: "Backend",
|
|
2123
|
+
orm: "Data",
|
|
2124
|
+
"database-client": "Data",
|
|
2125
|
+
"build-tool": "Tooling",
|
|
2126
|
+
bundler: "Tooling",
|
|
2127
|
+
linting: "Tooling",
|
|
2128
|
+
testing: "Tooling"
|
|
2129
|
+
};
|
|
2130
|
+
FLOW_LAYERS = ["Presentation", "Client Logic", "Backend", "Data"];
|
|
2131
|
+
BACKEND_FRAMEWORKS = /* @__PURE__ */ new Set([
|
|
2132
|
+
"express",
|
|
2133
|
+
"fastify",
|
|
2134
|
+
"koa",
|
|
2135
|
+
"hono",
|
|
2136
|
+
"@nestjs/core",
|
|
2137
|
+
"@hapi/hapi"
|
|
2138
|
+
]);
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
// src/mcp/server.ts
|
|
2143
|
+
var server_exports = {};
|
|
2144
|
+
__export(server_exports, {
|
|
2145
|
+
buildMcpServer: () => buildMcpServer,
|
|
2146
|
+
startMcpServer: () => startMcpServer
|
|
2147
|
+
});
|
|
2148
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2149
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2150
|
+
import { z as z2 } from "zod";
|
|
2151
|
+
function json(obj) {
|
|
2152
|
+
return { content: [{ type: "text", text: JSON.stringify(obj) }] };
|
|
2153
|
+
}
|
|
2154
|
+
function buildMcpServer(db) {
|
|
2155
|
+
const server = new McpServer({ name: SERVER_NAME, version: VERSION });
|
|
2156
|
+
server.registerTool(
|
|
2157
|
+
"recommend",
|
|
2158
|
+
{
|
|
2159
|
+
title: "Recommend packages",
|
|
2160
|
+
description: "Recommend the best current npm packages for a described need \u2014 fresh, objectively scored, with confidence labels. Call before choosing or hand-rolling a dependency.",
|
|
2161
|
+
inputSchema: {
|
|
2162
|
+
need: z2.string().min(1).describe("Natural-language description of what you need"),
|
|
2163
|
+
category: categoryEnum.optional().describe("Optional taxonomy category to restrict to"),
|
|
2164
|
+
constraints: constraintsSchema
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
async (args) => json(await handleRecommend(db, args))
|
|
2168
|
+
);
|
|
2169
|
+
server.registerTool(
|
|
2170
|
+
"evaluate",
|
|
2171
|
+
{
|
|
2172
|
+
title: "Evaluate a package",
|
|
2173
|
+
description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
|
|
2174
|
+
inputSchema: {
|
|
2175
|
+
package: z2.string().min(1).describe("npm package name")
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
async (args) => json(await handleEvaluate(db, args))
|
|
2179
|
+
);
|
|
2180
|
+
server.registerTool(
|
|
2181
|
+
"compare",
|
|
2182
|
+
{
|
|
2183
|
+
title: "Compare packages",
|
|
2184
|
+
description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
|
|
2185
|
+
inputSchema: {
|
|
2186
|
+
packages: z2.array(z2.string().min(1)).min(2).max(5).describe("2\u20135 npm package names")
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
async (args) => json(await handleCompare(db, args))
|
|
2190
|
+
);
|
|
2191
|
+
server.registerTool(
|
|
2192
|
+
"verify",
|
|
2193
|
+
{
|
|
2194
|
+
title: "Verify a package",
|
|
2195
|
+
description: "Confirm an npm package is real, healthy, and not risky before installing \u2014 guards against hallucinated or typosquatted dependency names. Checks the live registry.",
|
|
2196
|
+
inputSchema: {
|
|
2197
|
+
package: z2.string().min(1).describe("npm package name to verify")
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
async (args) => json(await handleVerify(db, args))
|
|
2201
|
+
);
|
|
2202
|
+
server.registerTool(
|
|
2203
|
+
"diagram",
|
|
2204
|
+
{
|
|
2205
|
+
title: "Reference architecture diagram",
|
|
2206
|
+
description: "Emit a reference-architecture Mermaid diagram for a stack you have already chosen (package names). A labeled starting point keyed by layer \u2014 not a validated architecture, and not an architecture designer.",
|
|
2207
|
+
inputSchema: {
|
|
2208
|
+
stack: z2.array(z2.string()).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
|
|
2209
|
+
}
|
|
2210
|
+
},
|
|
2211
|
+
async (args) => json(await handleDiagram(db, args))
|
|
2212
|
+
);
|
|
2213
|
+
return server;
|
|
2214
|
+
}
|
|
2215
|
+
async function startMcpServer() {
|
|
2216
|
+
const { db, close } = createDb();
|
|
2217
|
+
const server = buildMcpServer(db);
|
|
2218
|
+
const shutdown = async () => {
|
|
2219
|
+
try {
|
|
2220
|
+
await close();
|
|
2221
|
+
} finally {
|
|
2222
|
+
process.exit(0);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
process.on("SIGINT", shutdown);
|
|
2226
|
+
process.on("SIGTERM", shutdown);
|
|
2227
|
+
const transport = new StdioServerTransport();
|
|
2228
|
+
await server.connect(transport);
|
|
2229
|
+
logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
|
|
2230
|
+
}
|
|
2231
|
+
var categoryEnum, confidenceEnum, constraintsSchema;
|
|
2232
|
+
var init_server = __esm({
|
|
2233
|
+
"src/mcp/server.ts"() {
|
|
2234
|
+
"use strict";
|
|
2235
|
+
init_esm_shims();
|
|
2236
|
+
init_constants();
|
|
2237
|
+
init_types();
|
|
2238
|
+
init_client();
|
|
2239
|
+
init_logger();
|
|
2240
|
+
init_handlers();
|
|
2241
|
+
init_diagram();
|
|
2242
|
+
categoryEnum = z2.enum(CATEGORIES);
|
|
2243
|
+
confidenceEnum = z2.enum(["proven", "emerging", "unproven"]);
|
|
2244
|
+
constraintsSchema = z2.object({
|
|
2245
|
+
runtime: z2.enum(["browser", "node", "both"]).optional(),
|
|
2246
|
+
license: z2.string().optional(),
|
|
2247
|
+
maxBundleKb: z2.number().positive().optional(),
|
|
2248
|
+
minConfidence: confidenceEnum.optional()
|
|
2249
|
+
}).optional();
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
// src/pipeline/index.ts
|
|
2254
|
+
var pipeline_exports = {};
|
|
2255
|
+
__export(pipeline_exports, {
|
|
2256
|
+
runSync: () => runSync
|
|
2257
|
+
});
|
|
2258
|
+
var init_pipeline = __esm({
|
|
2259
|
+
"src/pipeline/index.ts"() {
|
|
2260
|
+
"use strict";
|
|
2261
|
+
init_esm_shims();
|
|
2262
|
+
init_sync();
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// src/cli/format.ts
|
|
2267
|
+
function table(headers, rows) {
|
|
2268
|
+
const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
|
|
2269
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
|
|
2270
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
|
|
2271
|
+
const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
|
|
2272
|
+
for (const r of rows) out.push(line(r));
|
|
2273
|
+
return out.join("\n");
|
|
2274
|
+
}
|
|
2275
|
+
function detail(pairs) {
|
|
2276
|
+
const labelWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
2277
|
+
return pairs.map(([k, v]) => `${dim((k + ":").padEnd(labelWidth + 1))} ${v}`).join("\n");
|
|
2278
|
+
}
|
|
2279
|
+
function formatNumber(n) {
|
|
2280
|
+
if (n == null) return "\u2014";
|
|
2281
|
+
if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
|
|
2282
|
+
if (n >= 1e3) return `${Math.round(n / 100) / 10}k`;
|
|
2283
|
+
return String(n);
|
|
2284
|
+
}
|
|
2285
|
+
function formatDate(iso) {
|
|
2286
|
+
return iso ? iso.slice(0, 10) : "\u2014";
|
|
2287
|
+
}
|
|
2288
|
+
function formatPercent(fraction) {
|
|
2289
|
+
if (fraction == null) return "\u2014";
|
|
2290
|
+
const pct = Math.round(fraction * 1e3) / 10;
|
|
2291
|
+
return `${pct > 0 ? "+" : ""}${pct}%`;
|
|
2292
|
+
}
|
|
2293
|
+
function confidenceLabel(c) {
|
|
2294
|
+
if (c === "proven") return green(c);
|
|
2295
|
+
if (c === "emerging") return yellow(c);
|
|
2296
|
+
return red(c);
|
|
2297
|
+
}
|
|
2298
|
+
var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
|
|
2299
|
+
var init_format = __esm({
|
|
2300
|
+
"src/cli/format.ts"() {
|
|
2301
|
+
"use strict";
|
|
2302
|
+
init_esm_shims();
|
|
2303
|
+
ESC = "\x1B[";
|
|
2304
|
+
wrap = (code, s) => `${ESC}${code}m${s}${ESC}0m`;
|
|
2305
|
+
bold = (s) => wrap("1", s);
|
|
2306
|
+
dim = (s) => wrap("2", s);
|
|
2307
|
+
red = (s) => wrap("31", s);
|
|
2308
|
+
green = (s) => wrap("32", s);
|
|
2309
|
+
yellow = (s) => wrap("33", s);
|
|
2310
|
+
ANSI_RE = /\[[0-9;]*m/g;
|
|
2311
|
+
width = (s) => s.replace(ANSI_RE, "").length;
|
|
2312
|
+
}
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
// src/cli/commands.ts
|
|
2316
|
+
var commands_exports = {};
|
|
2317
|
+
__export(commands_exports, {
|
|
2318
|
+
runCompare: () => runCompare,
|
|
2319
|
+
runEvaluate: () => runEvaluate,
|
|
2320
|
+
runRecommend: () => runRecommend,
|
|
2321
|
+
runVerify: () => runVerify
|
|
2322
|
+
});
|
|
2323
|
+
async function withDb(fn) {
|
|
2324
|
+
requireConfig(["DATABASE_URL"]);
|
|
2325
|
+
const handle = createDb();
|
|
2326
|
+
try {
|
|
2327
|
+
return await fn(handle.db);
|
|
2328
|
+
} finally {
|
|
2329
|
+
await handle.close();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
async function runRecommend(need, opts) {
|
|
2333
|
+
if (opts.category && !isCategory(opts.category)) {
|
|
2334
|
+
throw new Error(`Unknown category "${opts.category}".`);
|
|
2335
|
+
}
|
|
2336
|
+
await withDb(async (db) => {
|
|
2337
|
+
const res = await handleRecommend(db, {
|
|
2338
|
+
need,
|
|
2339
|
+
category: opts.category,
|
|
2340
|
+
constraints: opts.minConfidence ? { minConfidence: opts.minConfidence } : void 0
|
|
2341
|
+
});
|
|
2342
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
2343
|
+
if (res.candidates.length === 0) {
|
|
2344
|
+
console.log("No matching packages found.");
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
console.log(
|
|
2348
|
+
table(
|
|
2349
|
+
["Package", "Health", "Confidence", "Weekly", "Latest", "Category"],
|
|
2350
|
+
res.candidates.map((c) => [
|
|
2351
|
+
c.name,
|
|
2352
|
+
String(c.healthScore),
|
|
2353
|
+
confidenceLabel(c.confidence),
|
|
2354
|
+
formatNumber(c.weeklyDownloads),
|
|
2355
|
+
c.latestVersion ?? "\u2014",
|
|
2356
|
+
c.category ?? "\u2014"
|
|
2357
|
+
])
|
|
2358
|
+
)
|
|
2359
|
+
);
|
|
2360
|
+
console.log(dim(`
|
|
2361
|
+
data as of ${formatDate(res.dataAsOf)}`));
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
async function runEvaluate(pkg, opts) {
|
|
2365
|
+
await withDb(async (db) => {
|
|
2366
|
+
const res = await handleEvaluate(db, { package: pkg });
|
|
2367
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
2368
|
+
if ("tracked" in res) {
|
|
2369
|
+
console.log(res.suggestion);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
console.log(bold(res.name) + (res.category ? dim(` (${res.category})`) : ""));
|
|
2373
|
+
const b = res.scoreBreakdown;
|
|
2374
|
+
console.log(
|
|
2375
|
+
detail([
|
|
2376
|
+
["health", `${res.healthScore} ${confidenceLabel(res.confidence)}`],
|
|
2377
|
+
[
|
|
2378
|
+
"breakdown",
|
|
2379
|
+
`maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"}`
|
|
2380
|
+
],
|
|
2381
|
+
["version", res.latestVersion ?? "\u2014"],
|
|
2382
|
+
["weekly dl", `${formatNumber(res.weeklyDownloads)} (${formatPercent(res.downloadGrowth90d)} 90d)`],
|
|
2383
|
+
["scorecard", res.scorecard != null ? String(res.scorecard) : "\u2014"],
|
|
2384
|
+
["bundle", res.bundleMinGzipKb != null ? `${res.bundleMinGzipKb} KB gzip` : "\u2014"],
|
|
2385
|
+
["released", formatDate(res.lastReleaseAt)],
|
|
2386
|
+
["flags", [res.deprecated && "deprecated", res.archived && "archived"].filter(Boolean).join(", ") || "none"],
|
|
2387
|
+
["advisories", res.advisories.length ? res.advisories.map((a) => `${a.severity}`).join(", ") : "none"],
|
|
2388
|
+
["repo", res.repoUrl ?? "\u2014"]
|
|
2389
|
+
])
|
|
2390
|
+
);
|
|
2391
|
+
if (res.summary) console.log("\n" + res.summary);
|
|
2392
|
+
if (res.usageGuide) {
|
|
2393
|
+
const g = res.usageGuide;
|
|
2394
|
+
console.log(
|
|
2395
|
+
"\n" + detail(
|
|
2396
|
+
[
|
|
2397
|
+
["what", g.whatItIs],
|
|
2398
|
+
["when", g.whenToUse],
|
|
2399
|
+
g.whenNotToUse ? ["when not", g.whenNotToUse] : null,
|
|
2400
|
+
["fits", g.whereItFits],
|
|
2401
|
+
g.howToWireIn ? ["wire-in", g.howToWireIn] : null
|
|
2402
|
+
].filter(Boolean)
|
|
2403
|
+
)
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
console.log(dim(`
|
|
2407
|
+
data as of ${formatDate(res.dataAsOf)}${res.stale ? " (stale)" : ""}`));
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
async function runCompare(pkgs, opts) {
|
|
2411
|
+
await withDb(async (db) => {
|
|
2412
|
+
const res = await handleCompare(db, { packages: pkgs });
|
|
2413
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
2414
|
+
console.log(
|
|
2415
|
+
table(
|
|
2416
|
+
["Package", "Health", "Confidence", "Weekly", "90d", "Scorecard", "Released"],
|
|
2417
|
+
res.rows.map((r) => [
|
|
2418
|
+
r.name,
|
|
2419
|
+
String(r.healthScore),
|
|
2420
|
+
confidenceLabel(r.confidence),
|
|
2421
|
+
formatNumber(r.weeklyDownloads),
|
|
2422
|
+
formatPercent(r.downloadGrowth90d),
|
|
2423
|
+
r.scorecard != null ? String(r.scorecard) : "\u2014",
|
|
2424
|
+
formatDate(r.lastReleaseAt)
|
|
2425
|
+
])
|
|
2426
|
+
)
|
|
2427
|
+
);
|
|
2428
|
+
if (res.missing?.length) console.log(dim(`
|
|
2429
|
+
not found: ${res.missing.join(", ")}`));
|
|
2430
|
+
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
async function runVerify(pkg, opts) {
|
|
2434
|
+
await withDb(async (db) => {
|
|
2435
|
+
const res = await handleVerify(db, { package: pkg });
|
|
2436
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
2437
|
+
const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.deprecated || res.archived || res.advisoryCount > 0 ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
|
|
2438
|
+
console.log(`${bold(pkg)} ${verdict}`);
|
|
2439
|
+
console.log(
|
|
2440
|
+
detail([
|
|
2441
|
+
["version", res.latestVersion ?? "\u2014"],
|
|
2442
|
+
["weekly dl", formatNumber(res.weeklyDownloads)],
|
|
2443
|
+
["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
|
|
2444
|
+
["advisories", String(res.advisoryCount)],
|
|
2445
|
+
["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
|
|
2446
|
+
])
|
|
2447
|
+
);
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
var init_commands = __esm({
|
|
2451
|
+
"src/cli/commands.ts"() {
|
|
2452
|
+
"use strict";
|
|
2453
|
+
init_esm_shims();
|
|
2454
|
+
init_config();
|
|
2455
|
+
init_types();
|
|
2456
|
+
init_client();
|
|
2457
|
+
init_handlers();
|
|
2458
|
+
init_format();
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
// src/core/paths.ts
|
|
2463
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2464
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2465
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2466
|
+
function packageRoot() {
|
|
2467
|
+
if (cachedRoot) return cachedRoot;
|
|
2468
|
+
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
2469
|
+
for (; ; ) {
|
|
2470
|
+
if (existsSync2(join2(dir, "package.json"))) {
|
|
2471
|
+
cachedRoot = dir;
|
|
2472
|
+
return dir;
|
|
2473
|
+
}
|
|
2474
|
+
const parent = dirname2(dir);
|
|
2475
|
+
if (parent === dir) break;
|
|
2476
|
+
dir = parent;
|
|
2477
|
+
}
|
|
2478
|
+
cachedRoot = process.cwd();
|
|
2479
|
+
return cachedRoot;
|
|
2480
|
+
}
|
|
2481
|
+
function migrationsDir() {
|
|
2482
|
+
return join2(packageRoot(), "drizzle");
|
|
2483
|
+
}
|
|
2484
|
+
function seedJsonPath() {
|
|
2485
|
+
return join2(packageRoot(), "src", "data", "seed.json");
|
|
2486
|
+
}
|
|
2487
|
+
var cachedRoot;
|
|
2488
|
+
var init_paths = __esm({
|
|
2489
|
+
"src/core/paths.ts"() {
|
|
2490
|
+
"use strict";
|
|
2491
|
+
init_esm_shims();
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
|
|
2495
|
+
// src/cli/installSkill.ts
|
|
2496
|
+
var installSkill_exports = {};
|
|
2497
|
+
__export(installSkill_exports, {
|
|
2498
|
+
SUPPORTED_AGENTS: () => SUPPORTED_AGENTS,
|
|
2499
|
+
buildServerEntry: () => buildServerEntry,
|
|
2500
|
+
buildTomlBlock: () => buildTomlBlock,
|
|
2501
|
+
runInstallSkill: () => runInstallSkill
|
|
2502
|
+
});
|
|
2503
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2504
|
+
import { copyFileSync } from "fs";
|
|
2505
|
+
import { homedir as homedir2 } from "os";
|
|
2506
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2507
|
+
function home(...p) {
|
|
2508
|
+
return join3(homedir2(), ...p);
|
|
2509
|
+
}
|
|
2510
|
+
function agentSpecs() {
|
|
2511
|
+
return [
|
|
2512
|
+
{
|
|
2513
|
+
id: "claude-code",
|
|
2514
|
+
label: "Claude Code",
|
|
2515
|
+
format: "mcpServers",
|
|
2516
|
+
path: home(".claude.json"),
|
|
2517
|
+
detected: existsSync3(home(".claude.json")) || existsSync3(home(".claude"))
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
id: "cursor",
|
|
2521
|
+
label: "Cursor",
|
|
2522
|
+
format: "mcpServers",
|
|
2523
|
+
path: home(".cursor", "mcp.json"),
|
|
2524
|
+
detected: existsSync3(home(".cursor"))
|
|
2525
|
+
},
|
|
2526
|
+
{
|
|
2527
|
+
id: "windsurf",
|
|
2528
|
+
label: "Windsurf",
|
|
2529
|
+
format: "mcpServers",
|
|
2530
|
+
path: home(".codeium", "windsurf", "mcp_config.json"),
|
|
2531
|
+
detected: existsSync3(home(".codeium"))
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
id: "copilot",
|
|
2535
|
+
label: "VS Code / GitHub Copilot",
|
|
2536
|
+
format: "servers",
|
|
2537
|
+
path: home("Library", "Application Support", "Code", "User", "mcp.json"),
|
|
2538
|
+
detected: existsSync3(home("Library", "Application Support", "Code", "User"))
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
id: "codex",
|
|
2542
|
+
label: "OpenAI Codex CLI",
|
|
2543
|
+
format: "toml",
|
|
2544
|
+
path: home(".codex", "config.toml"),
|
|
2545
|
+
detected: existsSync3(home(".codex"))
|
|
2546
|
+
}
|
|
2547
|
+
];
|
|
2548
|
+
}
|
|
2549
|
+
function collectEnv() {
|
|
2550
|
+
const env = {};
|
|
2551
|
+
for (const key of ENV_KEYS) {
|
|
2552
|
+
const value = process.env[key];
|
|
2553
|
+
if (value) env[key] = value;
|
|
2554
|
+
}
|
|
2555
|
+
return env;
|
|
2556
|
+
}
|
|
2557
|
+
function readJsonObject(path2) {
|
|
2558
|
+
if (!existsSync3(path2)) return {};
|
|
2559
|
+
const text2 = readFileSync(path2, "utf8").trim();
|
|
2560
|
+
if (!text2) return {};
|
|
2561
|
+
return JSON.parse(text2);
|
|
2562
|
+
}
|
|
2563
|
+
function writeJson(path2, obj) {
|
|
2564
|
+
mkdirSync(dirname3(path2), { recursive: true });
|
|
2565
|
+
writeFileSync(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
2566
|
+
}
|
|
2567
|
+
function buildServerEntry(env, withType) {
|
|
2568
|
+
const entry = { command: "npx", args: ["-y", PACKAGE_NAME, "serve"] };
|
|
2569
|
+
if (withType) entry.type = "stdio";
|
|
2570
|
+
if (Object.keys(env).length) entry.env = env;
|
|
2571
|
+
return entry;
|
|
2572
|
+
}
|
|
2573
|
+
function installJson(spec, env) {
|
|
2574
|
+
const key = spec.format === "servers" ? "servers" : "mcpServers";
|
|
2575
|
+
const config = readJsonObject(spec.path);
|
|
2576
|
+
if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
|
|
2577
|
+
config[key].lurq = buildServerEntry(env, spec.format === "servers");
|
|
2578
|
+
writeJson(spec.path, config);
|
|
2579
|
+
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
2580
|
+
}
|
|
2581
|
+
function buildTomlBlock(env) {
|
|
2582
|
+
const lines = ["[mcp_servers.lurq]", 'command = "npx"', `args = ["-y", "${PACKAGE_NAME}", "serve"]`];
|
|
2583
|
+
if (Object.keys(env).length) {
|
|
2584
|
+
lines.push("", "[mcp_servers.lurq.env]");
|
|
2585
|
+
for (const [k, v] of Object.entries(env)) lines.push(`${k} = ${JSON.stringify(v)}`);
|
|
2586
|
+
}
|
|
2587
|
+
return lines.join("\n") + "\n";
|
|
2588
|
+
}
|
|
2589
|
+
function installToml(spec, env) {
|
|
2590
|
+
const existing = existsSync3(spec.path) ? readFileSync(spec.path, "utf8") : "";
|
|
2591
|
+
if (existing.includes("[mcp_servers.lurq]")) {
|
|
2592
|
+
return {
|
|
2593
|
+
agent: spec.id,
|
|
2594
|
+
path: spec.path,
|
|
2595
|
+
status: "skipped",
|
|
2596
|
+
message: "lurq already present; edit manually to change it."
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
mkdirSync(dirname3(spec.path), { recursive: true });
|
|
2600
|
+
const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
|
|
2601
|
+
writeFileSync(spec.path, existing + sep + buildTomlBlock(env), "utf8");
|
|
2602
|
+
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
2603
|
+
}
|
|
2604
|
+
function installInstructionsFile() {
|
|
2605
|
+
const src = join3(packageRoot(), "templates", "skill-instructions.md");
|
|
2606
|
+
if (!existsSync3(src)) return null;
|
|
2607
|
+
const destDir = home(".lurq");
|
|
2608
|
+
const dest = join3(destDir, "skill-instructions.md");
|
|
2609
|
+
mkdirSync(destDir, { recursive: true });
|
|
2610
|
+
copyFileSync(src, dest);
|
|
2611
|
+
return dest;
|
|
2612
|
+
}
|
|
2613
|
+
async function runInstallSkill(opts) {
|
|
2614
|
+
const target = opts.agent ?? "claude-code";
|
|
2615
|
+
const specs = agentSpecs();
|
|
2616
|
+
const env = collectEnv();
|
|
2617
|
+
let selected;
|
|
2618
|
+
if (target === "all") {
|
|
2619
|
+
selected = specs.filter((s) => s.detected);
|
|
2620
|
+
if (selected.length === 0) {
|
|
2621
|
+
console.log("No supported agents detected on this machine.");
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
} else {
|
|
2625
|
+
const spec = specs.find((s) => s.id === target);
|
|
2626
|
+
if (!spec) {
|
|
2627
|
+
throw new Error(
|
|
2628
|
+
`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
selected = [spec];
|
|
2632
|
+
}
|
|
2633
|
+
if (!env.DATABASE_URL) {
|
|
2634
|
+
logger.warn(
|
|
2635
|
+
"DATABASE_URL is not set in the current environment \u2014 the installed server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const results = [];
|
|
2639
|
+
for (const spec of selected) {
|
|
2640
|
+
try {
|
|
2641
|
+
results.push(spec.format === "toml" ? installToml(spec, env) : installJson(spec, env));
|
|
2642
|
+
} catch (err) {
|
|
2643
|
+
results.push({
|
|
2644
|
+
agent: spec.id,
|
|
2645
|
+
path: spec.path,
|
|
2646
|
+
status: "error",
|
|
2647
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const instructionsPath = installInstructionsFile();
|
|
2652
|
+
console.log("lurq MCP server registration:");
|
|
2653
|
+
for (const r of results) {
|
|
2654
|
+
const spec = specs.find((s) => s.id === r.agent);
|
|
2655
|
+
const mark = r.status === "installed" ? "\u2713" : r.status === "skipped" ? "\u2022" : "\u2717";
|
|
2656
|
+
console.log(` ${mark} ${spec.label.padEnd(26)} ${r.path}${r.message ? ` (${r.message})` : ""}`);
|
|
2657
|
+
}
|
|
2658
|
+
if (instructionsPath) console.log(`
|
|
2659
|
+
Skill instructions written to ${instructionsPath}`);
|
|
2660
|
+
console.log("\nNext steps:");
|
|
2661
|
+
console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
|
|
2662
|
+
console.log(" 2. Restart the agent so it picks up the new MCP server.");
|
|
2663
|
+
console.log(" 3. Ask it to recommend a library \u2014 it should call lurq.");
|
|
2664
|
+
}
|
|
2665
|
+
var ENV_KEYS, SUPPORTED_AGENTS;
|
|
2666
|
+
var init_installSkill = __esm({
|
|
2667
|
+
"src/cli/installSkill.ts"() {
|
|
2668
|
+
"use strict";
|
|
2669
|
+
init_esm_shims();
|
|
2670
|
+
init_constants();
|
|
2671
|
+
init_logger();
|
|
2672
|
+
init_paths();
|
|
2673
|
+
ENV_KEYS = [
|
|
2674
|
+
"DATABASE_URL",
|
|
2675
|
+
"GITHUB_TOKEN",
|
|
2676
|
+
"EMBEDDING_PROVIDER",
|
|
2677
|
+
"EMBEDDING_API_KEY",
|
|
2678
|
+
"EMBEDDING_MODEL",
|
|
2679
|
+
"SUMMARY_PROVIDER",
|
|
2680
|
+
"SUMMARY_API_KEY",
|
|
2681
|
+
"SUMMARY_MODEL"
|
|
2682
|
+
];
|
|
2683
|
+
SUPPORTED_AGENTS = ["claude-code", "cursor", "windsurf", "copilot", "codex"];
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
|
|
2687
|
+
// src/db/seed.ts
|
|
2688
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2689
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2690
|
+
import { z as z3 } from "zod";
|
|
2691
|
+
function loadSeedFile(path2 = seedJsonPath()) {
|
|
2692
|
+
const raw = JSON.parse(readFileSync2(path2, "utf8"));
|
|
2693
|
+
const parsed = SeedFileSchema.parse(raw);
|
|
2694
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2695
|
+
for (const entry of parsed) {
|
|
2696
|
+
if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
2697
|
+
}
|
|
2698
|
+
return [...byName.values()];
|
|
2699
|
+
}
|
|
2700
|
+
async function loadSeedPackages(db, path2) {
|
|
2701
|
+
const entries = loadSeedFile(path2);
|
|
2702
|
+
if (entries.length === 0) return 0;
|
|
2703
|
+
await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
|
|
2704
|
+
target: seedPackages.name,
|
|
2705
|
+
// Refresh category to the incoming value on conflict (EXCLUDED.category).
|
|
2706
|
+
set: { category: sql5`excluded.category` }
|
|
2707
|
+
});
|
|
2708
|
+
logger.info(`Loaded ${entries.length} packages into seed_packages.`);
|
|
2709
|
+
return entries.length;
|
|
2710
|
+
}
|
|
2711
|
+
var SeedEntrySchema, SeedFileSchema;
|
|
2712
|
+
var init_seed = __esm({
|
|
2713
|
+
"src/db/seed.ts"() {
|
|
2714
|
+
"use strict";
|
|
2715
|
+
init_esm_shims();
|
|
2716
|
+
init_types();
|
|
2717
|
+
init_logger();
|
|
2718
|
+
init_paths();
|
|
2719
|
+
init_schema();
|
|
2720
|
+
SeedEntrySchema = z3.object({
|
|
2721
|
+
name: z3.string().min(1),
|
|
2722
|
+
category: z3.enum(CATEGORIES).optional()
|
|
2723
|
+
});
|
|
2724
|
+
SeedFileSchema = z3.array(SeedEntrySchema);
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
// src/db/migrate.ts
|
|
2729
|
+
var migrate_exports = {};
|
|
2730
|
+
__export(migrate_exports, {
|
|
2731
|
+
runMigrate: () => runMigrate,
|
|
2732
|
+
runReset: () => runReset
|
|
2733
|
+
});
|
|
2734
|
+
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
2735
|
+
async function ensureVectorExtension(handle) {
|
|
2736
|
+
await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
|
|
2737
|
+
}
|
|
2738
|
+
async function runMigrate() {
|
|
2739
|
+
const handle = createDb({ max: 1 });
|
|
2740
|
+
try {
|
|
2741
|
+
logger.info("Ensuring pgvector extension\u2026");
|
|
2742
|
+
await ensureVectorExtension(handle);
|
|
2743
|
+
logger.info("Applying migrations\u2026");
|
|
2744
|
+
await migrate(handle.db, { migrationsFolder: migrationsDir() });
|
|
2745
|
+
logger.info("Loading seed list\u2026");
|
|
2746
|
+
await loadSeedPackages(handle.db);
|
|
2747
|
+
logger.info("Migration complete.");
|
|
2748
|
+
} finally {
|
|
2749
|
+
await handle.close();
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
async function runReset() {
|
|
2753
|
+
const handle = createDb({ max: 1 });
|
|
2754
|
+
try {
|
|
2755
|
+
logger.warn("Dropping schema `public` (destructive)\u2026");
|
|
2756
|
+
await handle.sql.unsafe("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;");
|
|
2757
|
+
await handle.sql`DROP SCHEMA IF EXISTS drizzle CASCADE`;
|
|
2758
|
+
} finally {
|
|
2759
|
+
await handle.close();
|
|
2760
|
+
}
|
|
2761
|
+
await runMigrate();
|
|
2762
|
+
logger.info("Reset complete.");
|
|
2763
|
+
}
|
|
2764
|
+
var init_migrate = __esm({
|
|
2765
|
+
"src/db/migrate.ts"() {
|
|
2766
|
+
"use strict";
|
|
2767
|
+
init_esm_shims();
|
|
2768
|
+
init_logger();
|
|
2769
|
+
init_paths();
|
|
2770
|
+
init_client();
|
|
2771
|
+
init_seed();
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
// src/bin/lurq.ts
|
|
2776
|
+
init_esm_shims();
|
|
2777
|
+
|
|
2778
|
+
// src/cli/index.ts
|
|
2779
|
+
init_esm_shims();
|
|
2780
|
+
init_constants();
|
|
2781
|
+
import { Command } from "commander";
|
|
2782
|
+
function buildProgram() {
|
|
2783
|
+
const program = new Command();
|
|
2784
|
+
program.name(SERVER_NAME).description(
|
|
2785
|
+
"lurq - a fresh, objectively-scored index of JS/TS packages for AI coding agents."
|
|
2786
|
+
).version(VERSION, "-v, --version", "output the lurq version");
|
|
2787
|
+
program.command("serve").description("start the MCP server over stdio (for agent integration)").action(async () => {
|
|
2788
|
+
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2789
|
+
await startMcpServer2();
|
|
2790
|
+
});
|
|
2791
|
+
program.command("sync").description("run ingestion: refresh scores for the seed list (or one package)").option("--full", "force a full re-sync, ignoring cache TTLs").option("--package <name>", "sync a single package by name").option("--json", "output the run summary as JSON").action(async (opts) => {
|
|
2792
|
+
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
2793
|
+
const summary = await runSync2({ full: opts.full, packageName: opts.package });
|
|
2794
|
+
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
2795
|
+
if (summary.status === "failed") process.exitCode = 1;
|
|
2796
|
+
});
|
|
2797
|
+
program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
|
|
2798
|
+
const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2799
|
+
await runRecommend2(need, opts);
|
|
2800
|
+
});
|
|
2801
|
+
program.command("evaluate").argument("<package>", "npm package name").description("full evidence read for one package (scores, signals, usage guide)").option("--json", "output JSON instead of a table").action(async (pkg, opts) => {
|
|
2802
|
+
const { runEvaluate: runEvaluate2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2803
|
+
await runEvaluate2(pkg, opts);
|
|
2804
|
+
});
|
|
2805
|
+
program.command("compare").argument("<packages...>", "2\u20135 npm package names").description("side-by-side comparison of packages, ranked by health").option("--json", "output JSON instead of a table").action(async (pkgs, opts) => {
|
|
2806
|
+
const { runCompare: runCompare2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2807
|
+
await runCompare2(pkgs, opts);
|
|
2808
|
+
});
|
|
2809
|
+
program.command("verify").argument("<package>", "npm package name").description("safety check: is this package real, healthy, and not risky?").option("--json", "output JSON instead of a table").action(async (pkg, opts) => {
|
|
2810
|
+
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2811
|
+
await runVerify2(pkg, opts);
|
|
2812
|
+
});
|
|
2813
|
+
program.command("install-skill").description("register lurq as an MCP server in supported AI assistants").option(
|
|
2814
|
+
"--agent <agent>",
|
|
2815
|
+
"claude-code | cursor | copilot | windsurf | codex | all",
|
|
2816
|
+
"claude-code"
|
|
2817
|
+
).action(async (opts) => {
|
|
2818
|
+
const { runInstallSkill: runInstallSkill2 } = await Promise.resolve().then(() => (init_installSkill(), installSkill_exports));
|
|
2819
|
+
await runInstallSkill2(opts);
|
|
2820
|
+
});
|
|
2821
|
+
const db = program.command("db").description("database management");
|
|
2822
|
+
db.command("migrate").description("apply database migrations and load the seed list").action(async () => {
|
|
2823
|
+
const { runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
2824
|
+
await runMigrate2();
|
|
2825
|
+
});
|
|
2826
|
+
db.command("reset").description("drop and recreate the schema (destructive)").option("--yes", "skip the confirmation prompt").action(async (opts) => {
|
|
2827
|
+
if (!opts.yes) {
|
|
2828
|
+
console.error(
|
|
2829
|
+
"Refusing to reset without confirmation. Re-run with `--yes` to drop and recreate the schema."
|
|
2830
|
+
);
|
|
2831
|
+
process.exitCode = 1;
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
const { runReset: runReset2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
2835
|
+
await runReset2();
|
|
2836
|
+
});
|
|
2837
|
+
return program;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// src/bin/lurq.ts
|
|
2841
|
+
init_config();
|
|
2842
|
+
init_logger();
|
|
2843
|
+
loadEnv();
|
|
2844
|
+
buildProgram().parseAsync(process.argv).catch((err) => {
|
|
2845
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2846
|
+
process.exit(1);
|
|
2847
|
+
});
|
|
2848
|
+
//# sourceMappingURL=lurq.js.map
|