lurqrun 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -101
- package/dist/bin/lurq.js +2040 -191
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +51 -2
- package/dist/index.js +1964 -191
- package/dist/index.js.map +1 -1
- package/drizzle/0003_violet_giant_girl.sql +14 -0
- package/drizzle/0004_slim_valeria_richards.sql +15 -0
- package/drizzle/0005_petite_luke_cage.sql +12 -0
- package/drizzle/0006_calm_nighthawk.sql +3 -0
- package/drizzle/0007_slimy_naoko.sql +1 -0
- package/drizzle/0008_peaceful_tigra.sql +1 -0
- package/drizzle/meta/0003_snapshot.json +667 -0
- package/drizzle/meta/0004_snapshot.json +769 -0
- package/drizzle/meta/0005_snapshot.json +864 -0
- package/drizzle/meta/0006_snapshot.json +882 -0
- package/drizzle/meta/0007_snapshot.json +876 -0
- package/drizzle/meta/0008_snapshot.json +882 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -213,10 +213,14 @@ var init_logger = __esm({
|
|
|
213
213
|
var schema_exports = {};
|
|
214
214
|
__export(schema_exports, {
|
|
215
215
|
apiKeys: () => apiKeys,
|
|
216
|
+
compatEdges: () => compatEdges,
|
|
216
217
|
discoveryQueue: () => discoveryQueue,
|
|
218
|
+
packageVersions: () => packageVersions,
|
|
217
219
|
packages: () => packages,
|
|
218
220
|
seedPackages: () => seedPackages,
|
|
219
|
-
syncRuns: () => syncRuns
|
|
221
|
+
syncRuns: () => syncRuns,
|
|
222
|
+
verificationRuns: () => verificationRuns,
|
|
223
|
+
watchState: () => watchState
|
|
220
224
|
});
|
|
221
225
|
import { sql } from "drizzle-orm";
|
|
222
226
|
import {
|
|
@@ -227,13 +231,15 @@ import {
|
|
|
227
231
|
integer,
|
|
228
232
|
jsonb,
|
|
229
233
|
pgTable,
|
|
234
|
+
primaryKey,
|
|
230
235
|
real,
|
|
231
236
|
serial,
|
|
232
237
|
text,
|
|
233
238
|
timestamp,
|
|
239
|
+
uniqueIndex,
|
|
234
240
|
vector
|
|
235
241
|
} from "drizzle-orm/pg-core";
|
|
236
|
-
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys;
|
|
242
|
+
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges;
|
|
237
243
|
var init_schema = __esm({
|
|
238
244
|
"src/db/schema.ts"() {
|
|
239
245
|
"use strict";
|
|
@@ -270,7 +276,6 @@ var init_schema = __esm({
|
|
|
270
276
|
// Adoption signals
|
|
271
277
|
weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
|
|
272
278
|
downloadGrowth90d: real("download_growth_90d"),
|
|
273
|
-
dependentsCount: integer("dependents_count"),
|
|
274
279
|
stars: integer("stars"),
|
|
275
280
|
openIssues: integer("open_issues"),
|
|
276
281
|
closedIssues: integer("closed_issues"),
|
|
@@ -278,6 +283,11 @@ var init_schema = __esm({
|
|
|
278
283
|
scorecard: real("scorecard"),
|
|
279
284
|
bundleMinGzipKb: real("bundle_min_gzip_kb"),
|
|
280
285
|
advisories: jsonb("advisories").$type(),
|
|
286
|
+
// Compatibility metadata (Tier-1): declared peer-deps + engines of the
|
|
287
|
+
// latest version, so a whole-stack peer-range check is one indexed query.
|
|
288
|
+
peerDependencies: jsonb("peer_dependencies").$type(),
|
|
289
|
+
peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
|
|
290
|
+
engines: jsonb("engines").$type(),
|
|
281
291
|
// Computed outputs
|
|
282
292
|
healthScore: integer("health_score"),
|
|
283
293
|
/** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
|
|
@@ -287,6 +297,10 @@ var init_schema = __esm({
|
|
|
287
297
|
scoreBreakdown: jsonb("score_breakdown").$type(),
|
|
288
298
|
usageGuide: jsonb("usage_guide").$type(),
|
|
289
299
|
embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
|
|
300
|
+
// Identity of the vector space `embedding` was produced in (e.g.
|
|
301
|
+
// `openai:text-embedding-3-small`, `local`). Vector search filters on the
|
|
302
|
+
// active provider so switching models can't compare incompatible spaces.
|
|
303
|
+
embeddingProvider: text("embedding_provider"),
|
|
290
304
|
// Lexical search vector (§3): name weighted highest (A), then category (B),
|
|
291
305
|
// then summary/description (C). Generated + STORED so it stays in sync with
|
|
292
306
|
// the row automatically; indexed with GIN for fast `@@` matching.
|
|
@@ -353,6 +367,61 @@ var init_schema = __esm({
|
|
|
353
367
|
},
|
|
354
368
|
(table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
|
|
355
369
|
);
|
|
370
|
+
packageVersions = pgTable(
|
|
371
|
+
"package_versions",
|
|
372
|
+
{
|
|
373
|
+
packageName: text("package_name").notNull(),
|
|
374
|
+
version: text("version").notNull(),
|
|
375
|
+
publishedAt: ts("published_at")
|
|
376
|
+
},
|
|
377
|
+
(table2) => [
|
|
378
|
+
primaryKey({ columns: [table2.packageName, table2.version] }),
|
|
379
|
+
index("package_versions_name_published_idx").on(table2.packageName, table2.publishedAt)
|
|
380
|
+
]
|
|
381
|
+
);
|
|
382
|
+
watchState = pgTable("watch_state", {
|
|
383
|
+
id: text("id").primaryKey(),
|
|
384
|
+
seq: text("seq").notNull(),
|
|
385
|
+
updatedAt: ts("updated_at")
|
|
386
|
+
});
|
|
387
|
+
verificationRuns = pgTable(
|
|
388
|
+
"verification_runs",
|
|
389
|
+
{
|
|
390
|
+
id: serial("id").primaryKey(),
|
|
391
|
+
packageName: text("package_name").notNull(),
|
|
392
|
+
version: text("version").notNull(),
|
|
393
|
+
driver: text("driver").notNull(),
|
|
394
|
+
moduleSystem: text("module_system").notNull(),
|
|
395
|
+
installed: boolean("installed").notNull(),
|
|
396
|
+
imported: boolean("imported"),
|
|
397
|
+
ranScripts: boolean("ran_scripts").notNull().default(false),
|
|
398
|
+
durationMs: integer("duration_ms"),
|
|
399
|
+
error: text("error"),
|
|
400
|
+
ranAt: ts("ran_at")
|
|
401
|
+
},
|
|
402
|
+
(table2) => [index("verification_runs_pkg_idx").on(table2.packageName, table2.version)]
|
|
403
|
+
);
|
|
404
|
+
compatEdges = pgTable(
|
|
405
|
+
"compat_edges",
|
|
406
|
+
{
|
|
407
|
+
id: serial("id").primaryKey(),
|
|
408
|
+
packageA: text("package_a").notNull(),
|
|
409
|
+
versionA: text("version_a").notNull(),
|
|
410
|
+
packageB: text("package_b").notNull(),
|
|
411
|
+
versionB: text("version_b").notNull(),
|
|
412
|
+
status: text("status").$type().notNull(),
|
|
413
|
+
driver: text("driver").notNull(),
|
|
414
|
+
ranAt: ts("ran_at")
|
|
415
|
+
},
|
|
416
|
+
(table2) => [
|
|
417
|
+
uniqueIndex("compat_edges_pair_idx").on(
|
|
418
|
+
table2.packageA,
|
|
419
|
+
table2.versionA,
|
|
420
|
+
table2.packageB,
|
|
421
|
+
table2.versionB
|
|
422
|
+
)
|
|
423
|
+
]
|
|
424
|
+
);
|
|
356
425
|
}
|
|
357
426
|
});
|
|
358
427
|
|
|
@@ -361,10 +430,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
361
430
|
import postgres from "postgres";
|
|
362
431
|
function createDb(opts = {}) {
|
|
363
432
|
const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
|
|
364
|
-
const
|
|
433
|
+
const sql6 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
|
|
365
434
|
} });
|
|
366
|
-
const db = drizzle(
|
|
367
|
-
return { db, sql:
|
|
435
|
+
const db = drizzle(sql6, { schema: schema_exports });
|
|
436
|
+
return { db, sql: sql6, close: () => sql6.end() };
|
|
368
437
|
}
|
|
369
438
|
var init_client = __esm({
|
|
370
439
|
"src/db/client.ts"() {
|
|
@@ -375,6 +444,131 @@ var init_client = __esm({
|
|
|
375
444
|
}
|
|
376
445
|
});
|
|
377
446
|
|
|
447
|
+
// src/core/cache.ts
|
|
448
|
+
var cache_exports = {};
|
|
449
|
+
__export(cache_exports, {
|
|
450
|
+
cached: () => cached2,
|
|
451
|
+
invalidateCache: () => invalidateCache
|
|
452
|
+
});
|
|
453
|
+
function ttlSeconds() {
|
|
454
|
+
const n = Number(process.env.LURQ_CACHE_TTL_SEC);
|
|
455
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_TTL_SEC;
|
|
456
|
+
}
|
|
457
|
+
async function getClient() {
|
|
458
|
+
if (!process.env.REDIS_URL) return null;
|
|
459
|
+
if (!clientPromise) {
|
|
460
|
+
clientPromise = (async () => {
|
|
461
|
+
try {
|
|
462
|
+
const { default: Redis } = await import("ioredis");
|
|
463
|
+
const client = new Redis(process.env.REDIS_URL, {
|
|
464
|
+
maxRetriesPerRequest: 1,
|
|
465
|
+
enableOfflineQueue: false,
|
|
466
|
+
lazyConnect: false,
|
|
467
|
+
// Railway's private network (redis.railway.internal) is IPv6-only and
|
|
468
|
+
// ioredis defaults to IPv4 — family:0 lets it resolve either stack, so
|
|
469
|
+
// both the private URL and a public/Upstash URL work unchanged.
|
|
470
|
+
family: 0
|
|
471
|
+
});
|
|
472
|
+
client.on("error", (err) => logger.warn(`redis: ${err.message}`));
|
|
473
|
+
logger.info("Response cache enabled (REDIS_URL set).");
|
|
474
|
+
return client;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
logger.warn(`redis disabled (init failed): ${err.message}`);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
})();
|
|
480
|
+
}
|
|
481
|
+
return clientPromise;
|
|
482
|
+
}
|
|
483
|
+
async function namespaceVersion(client) {
|
|
484
|
+
if (versionMemo && Date.now() - versionMemo.at < VERSION_MEMO_MS) return versionMemo.value;
|
|
485
|
+
const value = await client.get(VERSION_KEY).catch(() => null) ?? "0";
|
|
486
|
+
versionMemo = { value, at: Date.now() };
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
async function cached2(namespace, key, compute, opts = {}) {
|
|
490
|
+
const client = await getClient();
|
|
491
|
+
if (!client) return compute();
|
|
492
|
+
let fullKey;
|
|
493
|
+
try {
|
|
494
|
+
const version = await namespaceVersion(client);
|
|
495
|
+
fullKey = `lurq:${namespace}:${version}:${key}`;
|
|
496
|
+
const hit = await client.get(fullKey);
|
|
497
|
+
if (hit != null) return JSON.parse(hit);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
logger.warn(`redis read failed, bypassing cache: ${err.message}`);
|
|
500
|
+
return compute();
|
|
501
|
+
}
|
|
502
|
+
const value = await compute();
|
|
503
|
+
if (!opts.skipCache?.(value)) {
|
|
504
|
+
client.set(fullKey, JSON.stringify(value), "EX", ttlSeconds()).catch(() => {
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
async function invalidateCache() {
|
|
510
|
+
const client = await getClient();
|
|
511
|
+
if (!client) return;
|
|
512
|
+
try {
|
|
513
|
+
await client.incr(VERSION_KEY);
|
|
514
|
+
versionMemo = null;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
logger.warn(`redis invalidate failed: ${err.message}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
var VERSION_KEY, DEFAULT_TTL_SEC, VERSION_MEMO_MS, clientPromise, versionMemo;
|
|
520
|
+
var init_cache = __esm({
|
|
521
|
+
"src/core/cache.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
init_esm_shims();
|
|
524
|
+
init_logger();
|
|
525
|
+
VERSION_KEY = "lurq:cachever";
|
|
526
|
+
DEFAULT_TTL_SEC = 3600;
|
|
527
|
+
VERSION_MEMO_MS = 1e4;
|
|
528
|
+
clientPromise = null;
|
|
529
|
+
versionMemo = null;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/db/compat.ts
|
|
534
|
+
import { and, inArray } from "drizzle-orm";
|
|
535
|
+
async function getCompatMetadata(db, names) {
|
|
536
|
+
if (names.length === 0) return [];
|
|
537
|
+
return db.select({
|
|
538
|
+
name: packages.name,
|
|
539
|
+
latestVersion: packages.latestVersion,
|
|
540
|
+
peerDependencies: packages.peerDependencies,
|
|
541
|
+
peerDependenciesMeta: packages.peerDependenciesMeta,
|
|
542
|
+
engines: packages.engines
|
|
543
|
+
}).from(packages).where(inArray(packages.name, names));
|
|
544
|
+
}
|
|
545
|
+
function canonicalPair(a, b) {
|
|
546
|
+
const [low, high] = a.name <= b.name ? [a, b] : [b, a];
|
|
547
|
+
return { packageA: low.name, versionA: low.version, packageB: high.name, versionB: high.version };
|
|
548
|
+
}
|
|
549
|
+
async function upsertCompatEdge(db, edge) {
|
|
550
|
+
await db.insert(compatEdges).values(edge).onConflictDoUpdate({
|
|
551
|
+
target: [
|
|
552
|
+
compatEdges.packageA,
|
|
553
|
+
compatEdges.versionA,
|
|
554
|
+
compatEdges.packageB,
|
|
555
|
+
compatEdges.versionB
|
|
556
|
+
],
|
|
557
|
+
set: { status: edge.status, driver: edge.driver, ranAt: edge.ranAt }
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
async function getCompatEdges(db, names) {
|
|
561
|
+
if (names.length === 0) return [];
|
|
562
|
+
return db.select().from(compatEdges).where(and(inArray(compatEdges.packageA, names), inArray(compatEdges.packageB, names)));
|
|
563
|
+
}
|
|
564
|
+
var init_compat = __esm({
|
|
565
|
+
"src/db/compat.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
init_esm_shims();
|
|
568
|
+
init_schema();
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
378
572
|
// src/core/http.ts
|
|
379
573
|
import { createHash } from "crypto";
|
|
380
574
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -439,9 +633,9 @@ async function httpRequest(url, opts) {
|
|
|
439
633
|
} = opts;
|
|
440
634
|
const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
|
|
441
635
|
if (ttlMs > 0 && !bypassCacheRead) {
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
return { status:
|
|
636
|
+
const cached3 = await readCache(key, ttlMs);
|
|
637
|
+
if (cached3) {
|
|
638
|
+
return { status: cached3.status, data: decode(cached3.body, accept), fromCache: true };
|
|
445
639
|
}
|
|
446
640
|
}
|
|
447
641
|
const limiter = limiterFor(host);
|
|
@@ -626,9 +820,45 @@ function parseNpmRegistry(json2) {
|
|
|
626
820
|
hasTypes: detectTypes(name, latestManifest),
|
|
627
821
|
hasTestScript: detectTestScript(latestManifest),
|
|
628
822
|
directDependenciesCount: countDeps(latestManifest?.dependencies),
|
|
629
|
-
hasProvenance: detectProvenance(latestManifest)
|
|
823
|
+
hasProvenance: detectProvenance(latestManifest),
|
|
824
|
+
hasInstallScripts: detectInstallScripts(latestManifest),
|
|
825
|
+
peerDependencies: parseDepMap(latestManifest?.peerDependencies),
|
|
826
|
+
peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
|
|
827
|
+
engines: parseDepMap(latestManifest?.engines),
|
|
828
|
+
versionTimeline: parseVersionTimeline(json2)
|
|
630
829
|
};
|
|
631
830
|
}
|
|
831
|
+
function parseDepMap(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return null;
|
|
833
|
+
const out = {};
|
|
834
|
+
for (const [name, range] of Object.entries(value)) {
|
|
835
|
+
if (typeof range === "string" && range.trim() !== "") out[name] = range;
|
|
836
|
+
}
|
|
837
|
+
return Object.keys(out).length ? out : null;
|
|
838
|
+
}
|
|
839
|
+
function parsePeerMeta(value) {
|
|
840
|
+
if (!value || typeof value !== "object") return null;
|
|
841
|
+
const out = {};
|
|
842
|
+
for (const [name, meta] of Object.entries(value)) {
|
|
843
|
+
if (meta && typeof meta === "object" && "optional" in meta) {
|
|
844
|
+
out[name] = { optional: Boolean(meta.optional) };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return Object.keys(out).length ? out : null;
|
|
848
|
+
}
|
|
849
|
+
function detectInstallScripts(manifest) {
|
|
850
|
+
const scripts = manifest?.scripts;
|
|
851
|
+
if (!scripts || typeof scripts !== "object") return false;
|
|
852
|
+
return ["preinstall", "install", "postinstall"].some(
|
|
853
|
+
(hook) => typeof scripts[hook] === "string" && scripts[hook].trim() !== ""
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
function parseVersionTimeline(json2) {
|
|
857
|
+
const versions = json2?.versions;
|
|
858
|
+
if (!versions || typeof versions !== "object") return [];
|
|
859
|
+
const time = json2?.time ?? {};
|
|
860
|
+
return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
|
|
861
|
+
}
|
|
632
862
|
function parseKeywords(value) {
|
|
633
863
|
if (!Array.isArray(value)) return [];
|
|
634
864
|
return value.filter((k) => typeof k === "string");
|
|
@@ -1025,6 +1255,248 @@ var init_sources = __esm({
|
|
|
1025
1255
|
}
|
|
1026
1256
|
});
|
|
1027
1257
|
|
|
1258
|
+
// src/compat/members.ts
|
|
1259
|
+
async function assembleMembers(db, names) {
|
|
1260
|
+
const tracked = new Map((await getCompatMetadata(db, names)).map((r) => [r.name, r]));
|
|
1261
|
+
const members = [];
|
|
1262
|
+
const unverified = [];
|
|
1263
|
+
await Promise.all(
|
|
1264
|
+
names.map(async (name) => {
|
|
1265
|
+
const row = tracked.get(name);
|
|
1266
|
+
if (row) {
|
|
1267
|
+
members.push({
|
|
1268
|
+
name,
|
|
1269
|
+
version: row.latestVersion,
|
|
1270
|
+
peerDependencies: row.peerDependencies,
|
|
1271
|
+
peerDependenciesMeta: row.peerDependenciesMeta,
|
|
1272
|
+
engines: row.engines
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const reg = await fetchNpmRegistry(name).catch(() => null);
|
|
1277
|
+
if (reg) {
|
|
1278
|
+
members.push({
|
|
1279
|
+
name,
|
|
1280
|
+
version: reg.latestVersion,
|
|
1281
|
+
peerDependencies: reg.peerDependencies,
|
|
1282
|
+
peerDependenciesMeta: reg.peerDependenciesMeta,
|
|
1283
|
+
engines: reg.engines
|
|
1284
|
+
});
|
|
1285
|
+
} else {
|
|
1286
|
+
unverified.push(name);
|
|
1287
|
+
}
|
|
1288
|
+
})
|
|
1289
|
+
);
|
|
1290
|
+
return { members, unverified };
|
|
1291
|
+
}
|
|
1292
|
+
var init_members = __esm({
|
|
1293
|
+
"src/compat/members.ts"() {
|
|
1294
|
+
"use strict";
|
|
1295
|
+
init_esm_shims();
|
|
1296
|
+
init_compat();
|
|
1297
|
+
init_sources();
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// src/compat/peerCompat.ts
|
|
1302
|
+
import semver from "semver";
|
|
1303
|
+
function satisfiesRange(version, range) {
|
|
1304
|
+
if (!semver.validRange(range)) return null;
|
|
1305
|
+
const v = semver.valid(version) ? version : semver.coerce(version)?.version;
|
|
1306
|
+
if (!v) return null;
|
|
1307
|
+
return semver.satisfies(v, range, { includePrerelease: true });
|
|
1308
|
+
}
|
|
1309
|
+
function rangesIntersect(a, b) {
|
|
1310
|
+
if (!semver.validRange(a) || !semver.validRange(b)) return null;
|
|
1311
|
+
try {
|
|
1312
|
+
return semver.intersects(a, b, { includePrerelease: true });
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function resolveArchitectureCompat(members) {
|
|
1318
|
+
const conflicts = [];
|
|
1319
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const m of members) if (m.version) pinned.set(m.name, m.version);
|
|
1321
|
+
const constraints = [];
|
|
1322
|
+
for (const m of members) {
|
|
1323
|
+
if (!m.peerDependencies) continue;
|
|
1324
|
+
for (const [peer, range] of Object.entries(m.peerDependencies)) {
|
|
1325
|
+
constraints.push({
|
|
1326
|
+
requirer: m.name,
|
|
1327
|
+
peer,
|
|
1328
|
+
range,
|
|
1329
|
+
optional: Boolean(m.peerDependenciesMeta?.[peer]?.optional)
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const c of constraints) {
|
|
1334
|
+
const pv = pinned.get(c.peer);
|
|
1335
|
+
if (pv && satisfiesRange(pv, c.range) === false) {
|
|
1336
|
+
conflicts.push({
|
|
1337
|
+
source: "peer-deps",
|
|
1338
|
+
packages: [c.requirer, c.peer],
|
|
1339
|
+
detail: `${c.requirer} needs peer ${c.peer}@${c.range}, but the stack uses ${c.peer}@${pv}`
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const byPeer = /* @__PURE__ */ new Map();
|
|
1344
|
+
for (const c of constraints) {
|
|
1345
|
+
if (pinned.has(c.peer) || c.optional) continue;
|
|
1346
|
+
const arr = byPeer.get(c.peer);
|
|
1347
|
+
if (arr) arr.push(c);
|
|
1348
|
+
else byPeer.set(c.peer, [c]);
|
|
1349
|
+
}
|
|
1350
|
+
for (const [peer, cs] of byPeer) {
|
|
1351
|
+
for (let i = 0; i < cs.length; i++) {
|
|
1352
|
+
for (let j = i + 1; j < cs.length; j++) {
|
|
1353
|
+
if (cs[i].range !== cs[j].range && rangesIntersect(cs[i].range, cs[j].range) === false) {
|
|
1354
|
+
conflicts.push({
|
|
1355
|
+
source: "peer-deps",
|
|
1356
|
+
packages: [cs[i].requirer, cs[j].requirer],
|
|
1357
|
+
detail: `${cs[i].requirer} needs ${peer}@${cs[i].range} but ${cs[j].requirer} needs ${peer}@${cs[j].range} \u2014 no overlapping version`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const nodeReqs = members.map((m) => ({ name: m.name, range: m.engines?.node })).filter((r) => typeof r.range === "string");
|
|
1364
|
+
for (let i = 0; i < nodeReqs.length; i++) {
|
|
1365
|
+
for (let j = i + 1; j < nodeReqs.length; j++) {
|
|
1366
|
+
if (rangesIntersect(nodeReqs[i].range, nodeReqs[j].range) === false) {
|
|
1367
|
+
conflicts.push({
|
|
1368
|
+
source: "engines",
|
|
1369
|
+
packages: [nodeReqs[i].name, nodeReqs[j].name],
|
|
1370
|
+
detail: `${nodeReqs[i].name} needs node ${nodeReqs[i].range} but ${nodeReqs[j].name} needs node ${nodeReqs[j].range} \u2014 no overlap`
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return conflicts;
|
|
1376
|
+
}
|
|
1377
|
+
var init_peerCompat = __esm({
|
|
1378
|
+
"src/compat/peerCompat.ts"() {
|
|
1379
|
+
"use strict";
|
|
1380
|
+
init_esm_shims();
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// src/compat/check.ts
|
|
1385
|
+
async function checkCompat(db, packages2) {
|
|
1386
|
+
const names = [...new Set(packages2)];
|
|
1387
|
+
const { members, unverified } = await assembleMembers(db, names);
|
|
1388
|
+
const conflicts = resolveArchitectureCompat(members);
|
|
1389
|
+
for (const edge of await getCompatEdges(db, names)) {
|
|
1390
|
+
if (edge.status === "conflict") {
|
|
1391
|
+
conflicts.push({
|
|
1392
|
+
source: "sandbox",
|
|
1393
|
+
packages: [edge.packageA, edge.packageB],
|
|
1394
|
+
detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const overall = conflicts.length ? "conflict" : unverified.length ? "unknown" : "compatible";
|
|
1399
|
+
return {
|
|
1400
|
+
packages: names,
|
|
1401
|
+
overall,
|
|
1402
|
+
conflicts,
|
|
1403
|
+
unverified,
|
|
1404
|
+
checked: members.map((m) => ({ name: m.name, version: m.version }))
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
var init_check = __esm({
|
|
1408
|
+
"src/compat/check.ts"() {
|
|
1409
|
+
"use strict";
|
|
1410
|
+
init_esm_shims();
|
|
1411
|
+
init_compat();
|
|
1412
|
+
init_members();
|
|
1413
|
+
init_peerCompat();
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// src/db/packages.ts
|
|
1418
|
+
import { desc, eq, isNotNull } from "drizzle-orm";
|
|
1419
|
+
async function getSeedTargets(db) {
|
|
1420
|
+
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1421
|
+
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
1422
|
+
}
|
|
1423
|
+
async function getPackageByName(db, name) {
|
|
1424
|
+
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
1425
|
+
return rows[0] ?? null;
|
|
1426
|
+
}
|
|
1427
|
+
async function getAllPackageNames(db) {
|
|
1428
|
+
const rows = await db.select({ name: packages.name }).from(packages);
|
|
1429
|
+
return rows.map((r) => r.name);
|
|
1430
|
+
}
|
|
1431
|
+
async function upsertPackageVersions(db, name, versions) {
|
|
1432
|
+
if (versions.length === 0) return;
|
|
1433
|
+
for (let i = 0; i < versions.length; i += VERSION_CHUNK) {
|
|
1434
|
+
const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
|
|
1435
|
+
packageName: name,
|
|
1436
|
+
version: v.version,
|
|
1437
|
+
publishedAt: v.publishedAt
|
|
1438
|
+
}));
|
|
1439
|
+
await db.insert(packageVersions).values(rows).onConflictDoNothing();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function getPackageVersions(db, name, limit = 50) {
|
|
1443
|
+
const rows = await db.select({ version: packageVersions.version, publishedAt: packageVersions.publishedAt }).from(packageVersions).where(eq(packageVersions.packageName, name)).orderBy(desc(packageVersions.publishedAt)).limit(limit);
|
|
1444
|
+
return rows.map((r) => ({ version: r.version, publishedAt: r.publishedAt }));
|
|
1445
|
+
}
|
|
1446
|
+
async function getTopPackageNames(db, limit = 1e3) {
|
|
1447
|
+
const rows = await db.select({ name: packages.name }).from(packages).where(isNotNull(packages.weeklyDownloads)).orderBy(desc(packages.weeklyDownloads)).limit(limit);
|
|
1448
|
+
return rows.map((r) => r.name);
|
|
1449
|
+
}
|
|
1450
|
+
async function ensureSeedEntry(db, name, category) {
|
|
1451
|
+
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1452
|
+
}
|
|
1453
|
+
async function upsertPackage(db, row) {
|
|
1454
|
+
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
1455
|
+
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1456
|
+
target: packages.name,
|
|
1457
|
+
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async function startSyncRun(db) {
|
|
1461
|
+
const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
|
|
1462
|
+
return row.id;
|
|
1463
|
+
}
|
|
1464
|
+
async function finishSyncRun(db, id, data) {
|
|
1465
|
+
await db.update(syncRuns).set({
|
|
1466
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1467
|
+
packagesSeen: data.packagesSeen,
|
|
1468
|
+
packagesUpdated: data.packagesUpdated,
|
|
1469
|
+
errors: data.errors,
|
|
1470
|
+
status: data.status
|
|
1471
|
+
}).where(eq(syncRuns.id, id));
|
|
1472
|
+
}
|
|
1473
|
+
var VERSION_CHUNK;
|
|
1474
|
+
var init_packages = __esm({
|
|
1475
|
+
"src/db/packages.ts"() {
|
|
1476
|
+
"use strict";
|
|
1477
|
+
init_esm_shims();
|
|
1478
|
+
init_schema();
|
|
1479
|
+
VERSION_CHUNK = 500;
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
// src/db/verification.ts
|
|
1484
|
+
import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
|
|
1485
|
+
async function storeVerificationRun(db, run) {
|
|
1486
|
+
await db.insert(verificationRuns).values(run);
|
|
1487
|
+
}
|
|
1488
|
+
async function getLatestVerificationByName(db, packageName) {
|
|
1489
|
+
const rows = await db.select().from(verificationRuns).where(eq2(verificationRuns.packageName, packageName)).orderBy(desc2(verificationRuns.ranAt)).limit(1);
|
|
1490
|
+
return rows[0] ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
var init_verification = __esm({
|
|
1493
|
+
"src/db/verification.ts"() {
|
|
1494
|
+
"use strict";
|
|
1495
|
+
init_esm_shims();
|
|
1496
|
+
init_schema();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1028
1500
|
// src/ingestion/sources/githubReadme.ts
|
|
1029
1501
|
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
1030
1502
|
for (const file of CANDIDATES) {
|
|
@@ -1422,10 +1894,10 @@ function settableKeys() {
|
|
|
1422
1894
|
function applyOverrides(base, sets) {
|
|
1423
1895
|
const next = structuredClone(base);
|
|
1424
1896
|
for (const entry of sets) {
|
|
1425
|
-
const
|
|
1426
|
-
if (
|
|
1427
|
-
const key = entry.slice(0,
|
|
1428
|
-
const value = Number(entry.slice(
|
|
1897
|
+
const eq9 = entry.indexOf("=");
|
|
1898
|
+
if (eq9 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
|
|
1899
|
+
const key = entry.slice(0, eq9).trim();
|
|
1900
|
+
const value = Number(entry.slice(eq9 + 1).trim());
|
|
1429
1901
|
const apply = SETTABLE[key];
|
|
1430
1902
|
if (!apply) {
|
|
1431
1903
|
throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
|
|
@@ -1589,8 +2061,6 @@ function toScoringInput(signals, category) {
|
|
|
1589
2061
|
weeklyDownloads: downloads?.weeklyDownloads ?? null,
|
|
1590
2062
|
downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
|
|
1591
2063
|
stars: github?.stars ?? null,
|
|
1592
|
-
dependentsCount: null,
|
|
1593
|
-
// not collected in v1 (see spec R1 note)
|
|
1594
2064
|
firstPublishedAt: registry?.firstPublishedAt ?? null,
|
|
1595
2065
|
lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
|
|
1596
2066
|
releasesLast12mo: github?.releasesLast12mo ?? null,
|
|
@@ -1746,7 +2216,8 @@ function computeConfidence(input, now, qualityScore = null) {
|
|
|
1746
2216
|
if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
|
|
1747
2217
|
return "emerging";
|
|
1748
2218
|
}
|
|
1749
|
-
const
|
|
2219
|
+
const promisingRecencyMonths = lastReleaseMonths ?? ageMonths;
|
|
2220
|
+
const promisingReleaseOk = promisingRecencyMonths !== null && promisingRecencyMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
|
|
1750
2221
|
if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
|
|
1751
2222
|
return "promising";
|
|
1752
2223
|
}
|
|
@@ -1859,6 +2330,7 @@ var init_embeddings = __esm({
|
|
|
1859
2330
|
LocalEmbeddingProvider = class {
|
|
1860
2331
|
kind = "local";
|
|
1861
2332
|
dimensions = EMBEDDING_DIM;
|
|
2333
|
+
id = "local";
|
|
1862
2334
|
async embed(texts) {
|
|
1863
2335
|
return texts.map((t) => localEmbed(t));
|
|
1864
2336
|
}
|
|
@@ -1869,17 +2341,19 @@ var init_embeddings = __esm({
|
|
|
1869
2341
|
this.apiKey = apiKey;
|
|
1870
2342
|
this.model = model;
|
|
1871
2343
|
this.fetchImpl = fetchImpl;
|
|
2344
|
+
this.id = `openai:${model}`;
|
|
1872
2345
|
}
|
|
1873
2346
|
apiKey;
|
|
1874
2347
|
model;
|
|
1875
2348
|
fetchImpl;
|
|
1876
2349
|
kind = "openai";
|
|
1877
2350
|
dimensions = EMBEDDING_DIM;
|
|
2351
|
+
id;
|
|
1878
2352
|
async embed(texts) {
|
|
1879
2353
|
const out = [];
|
|
1880
2354
|
for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
|
|
1881
2355
|
const batch = texts.slice(i, i + OPENAI_BATCH);
|
|
1882
|
-
const body = JSON.stringify({ model: this.model, input: batch });
|
|
2356
|
+
const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
|
|
1883
2357
|
const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
|
|
1884
2358
|
host: "api.openai.com",
|
|
1885
2359
|
method: "POST",
|
|
@@ -1892,7 +2366,14 @@ var init_embeddings = __esm({
|
|
|
1892
2366
|
});
|
|
1893
2367
|
const vectors = data?.data ?? [];
|
|
1894
2368
|
vectors.sort((a, b) => a.index - b.index);
|
|
1895
|
-
for (const v of vectors)
|
|
2369
|
+
for (const v of vectors) {
|
|
2370
|
+
if (v.embedding.length !== EMBEDDING_DIM) {
|
|
2371
|
+
throw new Error(
|
|
2372
|
+
`OpenAI model ${this.model} returned ${v.embedding.length}-dim vectors; expected ${EMBEDDING_DIM}. Set EMBEDDING_MODEL to a text-embedding-3 model or update EMBEDDING_DIM.`
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
out.push(v.embedding);
|
|
2376
|
+
}
|
|
1896
2377
|
}
|
|
1897
2378
|
return out;
|
|
1898
2379
|
}
|
|
@@ -1900,47 +2381,6 @@ var init_embeddings = __esm({
|
|
|
1900
2381
|
}
|
|
1901
2382
|
});
|
|
1902
2383
|
|
|
1903
|
-
// src/db/packages.ts
|
|
1904
|
-
import { eq, sql as sql2 } from "drizzle-orm";
|
|
1905
|
-
async function getSeedTargets(db) {
|
|
1906
|
-
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1907
|
-
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
1908
|
-
}
|
|
1909
|
-
async function getPackageByName(db, name) {
|
|
1910
|
-
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
1911
|
-
return rows[0] ?? null;
|
|
1912
|
-
}
|
|
1913
|
-
async function ensureSeedEntry(db, name, category) {
|
|
1914
|
-
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1915
|
-
}
|
|
1916
|
-
async function upsertPackage(db, row) {
|
|
1917
|
-
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
1918
|
-
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1919
|
-
target: packages.name,
|
|
1920
|
-
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
async function startSyncRun(db) {
|
|
1924
|
-
const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
|
|
1925
|
-
return row.id;
|
|
1926
|
-
}
|
|
1927
|
-
async function finishSyncRun(db, id, data) {
|
|
1928
|
-
await db.update(syncRuns).set({
|
|
1929
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
1930
|
-
packagesSeen: data.packagesSeen,
|
|
1931
|
-
packagesUpdated: data.packagesUpdated,
|
|
1932
|
-
errors: data.errors,
|
|
1933
|
-
status: data.status
|
|
1934
|
-
}).where(eq(syncRuns.id, id));
|
|
1935
|
-
}
|
|
1936
|
-
var init_packages = __esm({
|
|
1937
|
-
"src/db/packages.ts"() {
|
|
1938
|
-
"use strict";
|
|
1939
|
-
init_esm_shims();
|
|
1940
|
-
init_schema();
|
|
1941
|
-
}
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
2384
|
// src/core/concurrency.ts
|
|
1945
2385
|
async function pMap(items, mapper, concurrency) {
|
|
1946
2386
|
const results = new Array(items.length);
|
|
@@ -2068,6 +2508,7 @@ async function runSync(opts = {}) {
|
|
|
2068
2508
|
healthScore,
|
|
2069
2509
|
qualityScore: c.quality,
|
|
2070
2510
|
embedding: embeddings[i] ?? null,
|
|
2511
|
+
embeddingProvider: embProvider.id,
|
|
2071
2512
|
now
|
|
2072
2513
|
})
|
|
2073
2514
|
);
|
|
@@ -2081,6 +2522,7 @@ async function runSync(opts = {}) {
|
|
|
2081
2522
|
status
|
|
2082
2523
|
});
|
|
2083
2524
|
logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
|
|
2525
|
+
if (updated > 0) await invalidateCache();
|
|
2084
2526
|
return { seen: targets.length, updated, errors: allErrors.length, status };
|
|
2085
2527
|
} catch (err) {
|
|
2086
2528
|
await finishSyncRun(handle.db, runId, {
|
|
@@ -2140,19 +2582,22 @@ function assemblePackageRow(p) {
|
|
|
2140
2582
|
lastReleaseAt: p.input.lastReleaseAt,
|
|
2141
2583
|
weeklyDownloads: p.input.weeklyDownloads,
|
|
2142
2584
|
downloadGrowth90d: p.input.downloadGrowth90d,
|
|
2143
|
-
dependentsCount: p.input.dependentsCount,
|
|
2144
2585
|
stars: p.input.stars,
|
|
2145
2586
|
openIssues: p.input.openIssues,
|
|
2146
2587
|
closedIssues: p.input.closedIssues,
|
|
2147
2588
|
scorecard: p.input.scorecard,
|
|
2148
2589
|
bundleMinGzipKb: p.input.bundleMinGzipKb,
|
|
2149
2590
|
advisories: p.input.advisories,
|
|
2591
|
+
peerDependencies: r?.peerDependencies ?? null,
|
|
2592
|
+
peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
|
|
2593
|
+
engines: r?.engines ?? null,
|
|
2150
2594
|
healthScore: p.healthScore,
|
|
2151
2595
|
qualityScore: p.qualityScore,
|
|
2152
2596
|
confidence: p.confidence,
|
|
2153
2597
|
scoreBreakdown: p.breakdown,
|
|
2154
2598
|
usageGuide: p.usageGuide,
|
|
2155
2599
|
embedding: p.embedding,
|
|
2600
|
+
embeddingProvider: p.embedding ? p.embeddingProvider : null,
|
|
2156
2601
|
dataAsOf: p.now
|
|
2157
2602
|
};
|
|
2158
2603
|
}
|
|
@@ -2160,6 +2605,7 @@ var init_sync = __esm({
|
|
|
2160
2605
|
"src/pipeline/sync.ts"() {
|
|
2161
2606
|
"use strict";
|
|
2162
2607
|
init_esm_shims();
|
|
2608
|
+
init_cache();
|
|
2163
2609
|
init_config();
|
|
2164
2610
|
init_concurrency();
|
|
2165
2611
|
init_http();
|
|
@@ -2177,15 +2623,15 @@ var init_sync = __esm({
|
|
|
2177
2623
|
});
|
|
2178
2624
|
|
|
2179
2625
|
// src/pipeline/single.ts
|
|
2180
|
-
import { and, eq as
|
|
2626
|
+
import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
|
|
2181
2627
|
async function getSeedCategory(db, name) {
|
|
2182
|
-
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(
|
|
2628
|
+
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq3(seedPackages.name, name)).limit(1);
|
|
2183
2629
|
return row?.category ?? null;
|
|
2184
2630
|
}
|
|
2185
2631
|
async function getCategoryMedianBundle(db, category) {
|
|
2186
2632
|
const [row] = await db.select({
|
|
2187
|
-
m:
|
|
2188
|
-
}).from(packages).where(
|
|
2633
|
+
m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
2634
|
+
}).from(packages).where(and3(eq3(packages.category, category), isNotNull2(packages.bundleMinGzipKb)));
|
|
2189
2635
|
return row?.m ?? null;
|
|
2190
2636
|
}
|
|
2191
2637
|
async function syncOnePackage(db, name, opts = {}) {
|
|
@@ -2225,7 +2671,8 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
2225
2671
|
};
|
|
2226
2672
|
const healthScore = computeHealthScore(breakdown);
|
|
2227
2673
|
const confidence = computeConfidence(input, now, quality);
|
|
2228
|
-
const
|
|
2674
|
+
const embProvider = createEmbeddingProvider();
|
|
2675
|
+
const [embedding] = await embProvider.embed([
|
|
2229
2676
|
buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
|
|
2230
2677
|
]);
|
|
2231
2678
|
await upsertPackage(
|
|
@@ -2243,9 +2690,14 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
2243
2690
|
healthScore,
|
|
2244
2691
|
qualityScore: quality,
|
|
2245
2692
|
embedding: embedding ?? null,
|
|
2693
|
+
embeddingProvider: embProvider.id,
|
|
2246
2694
|
now
|
|
2247
2695
|
})
|
|
2248
2696
|
);
|
|
2697
|
+
await upsertPackageVersions(db, name, signals.registry?.versionTimeline ?? []).catch(
|
|
2698
|
+
() => {
|
|
2699
|
+
}
|
|
2700
|
+
);
|
|
2249
2701
|
return await getPackageByName(db, name);
|
|
2250
2702
|
}
|
|
2251
2703
|
async function getOrFetchPackage(db, name) {
|
|
@@ -2254,7 +2706,9 @@ async function getOrFetchPackage(db, name) {
|
|
|
2254
2706
|
const exists = await npmPackageExists(name);
|
|
2255
2707
|
if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
|
|
2256
2708
|
const row = await syncOnePackage(db, name);
|
|
2257
|
-
|
|
2709
|
+
if (row.confidence && row.confidence !== "unproven") {
|
|
2710
|
+
await ensureSeedEntry(db, name, row.category);
|
|
2711
|
+
}
|
|
2258
2712
|
return { row, wasTracked: false, existsOnNpm: true };
|
|
2259
2713
|
}
|
|
2260
2714
|
var init_single = __esm({
|
|
@@ -2275,16 +2729,16 @@ var init_single = __esm({
|
|
|
2275
2729
|
});
|
|
2276
2730
|
|
|
2277
2731
|
// src/search/recommend.ts
|
|
2278
|
-
import { and as
|
|
2732
|
+
import { and as and4, cosineDistance, eq as eq4, isNotNull as isNotNull3, lte, sql as sql3 } from "drizzle-orm";
|
|
2279
2733
|
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
2280
2734
|
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
2281
2735
|
const [queryVec] = await provider.embed([opts.need]);
|
|
2282
2736
|
if (!queryVec) return [];
|
|
2283
2737
|
const category = opts.category ?? inferCategory(opts.need);
|
|
2284
2738
|
const pool = Math.max(limit * 5, 25);
|
|
2285
|
-
let fused = await hybridSearch(db, queryVec, opts.need, opts.constraints, category, pool);
|
|
2739
|
+
let fused = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, category, pool);
|
|
2286
2740
|
if (category && fused.length < limit) {
|
|
2287
|
-
const broad = await hybridSearch(db, queryVec, opts.need, opts.constraints, null, pool);
|
|
2741
|
+
const broad = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, null, pool);
|
|
2288
2742
|
const seen = new Set(fused.map((f) => f.row.name));
|
|
2289
2743
|
fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
|
|
2290
2744
|
}
|
|
@@ -2298,9 +2752,9 @@ async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
|
2298
2752
|
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2299
2753
|
return ranked.map(({ row }) => toCandidate(row));
|
|
2300
2754
|
}
|
|
2301
|
-
async function hybridSearch(db, queryVec, need, constraints, category, pool) {
|
|
2755
|
+
async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
|
|
2302
2756
|
const [vectorRows, lexicalRows] = await Promise.all([
|
|
2303
|
-
runVectorQuery(db, queryVec, constraints, category, pool),
|
|
2757
|
+
runVectorQuery(db, queryVec, providerId, constraints, category, pool),
|
|
2304
2758
|
runLexicalQuery(db, need, constraints, category, pool)
|
|
2305
2759
|
]);
|
|
2306
2760
|
return rrfFuse([vectorRows, lexicalRows]);
|
|
@@ -2319,8 +2773,8 @@ function rrfFuse(lists, k = RRF_K) {
|
|
|
2319
2773
|
}
|
|
2320
2774
|
function buildConditions(constraints, category) {
|
|
2321
2775
|
const conditions = [];
|
|
2322
|
-
if (category) conditions.push(
|
|
2323
|
-
if (constraints?.license) conditions.push(
|
|
2776
|
+
if (category) conditions.push(eq4(packages.category, category));
|
|
2777
|
+
if (constraints?.license) conditions.push(eq4(packages.license, constraints.license));
|
|
2324
2778
|
if (constraints?.maxBundleKb !== void 0) {
|
|
2325
2779
|
conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
|
|
2326
2780
|
}
|
|
@@ -2329,21 +2783,25 @@ function buildConditions(constraints, category) {
|
|
|
2329
2783
|
(c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
|
|
2330
2784
|
);
|
|
2331
2785
|
conditions.push(
|
|
2332
|
-
|
|
2786
|
+
sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
|
|
2333
2787
|
);
|
|
2334
2788
|
}
|
|
2335
2789
|
return conditions;
|
|
2336
2790
|
}
|
|
2337
|
-
async function runVectorQuery(db, queryVec, constraints, category, pool) {
|
|
2791
|
+
async function runVectorQuery(db, queryVec, providerId, constraints, category, pool) {
|
|
2338
2792
|
const distance = cosineDistance(packages.embedding, queryVec);
|
|
2339
|
-
const conditions = [
|
|
2340
|
-
|
|
2793
|
+
const conditions = [
|
|
2794
|
+
isNotNull3(packages.embedding),
|
|
2795
|
+
eq4(packages.embeddingProvider, providerId),
|
|
2796
|
+
...buildConditions(constraints, category)
|
|
2797
|
+
];
|
|
2798
|
+
return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(distance).limit(pool);
|
|
2341
2799
|
}
|
|
2342
2800
|
async function runLexicalQuery(db, need, constraints, category, pool) {
|
|
2343
|
-
const tsquery =
|
|
2344
|
-
const rank =
|
|
2345
|
-
const conditions = [
|
|
2346
|
-
return db.select(ROW_COLUMNS).from(packages).where(
|
|
2801
|
+
const tsquery = sql3`websearch_to_tsquery('english', ${need})`;
|
|
2802
|
+
const rank = sql3`ts_rank(${packages.searchVector}, ${tsquery})`;
|
|
2803
|
+
const conditions = [sql3`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
|
|
2804
|
+
return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(sql3`${rank} desc`).limit(pool);
|
|
2347
2805
|
}
|
|
2348
2806
|
function toCandidate(row) {
|
|
2349
2807
|
return {
|
|
@@ -2404,12 +2862,99 @@ var init_recommend = __esm({
|
|
|
2404
2862
|
}
|
|
2405
2863
|
});
|
|
2406
2864
|
|
|
2865
|
+
// src/security/risk.ts
|
|
2866
|
+
function assessRisk(i) {
|
|
2867
|
+
const malwarePattern = i.installScripts && i.brandNew && i.lowTrust;
|
|
2868
|
+
if (i.typosquat || i.hasCriticalOrHighAdvisory || malwarePattern) return "high";
|
|
2869
|
+
if (i.deprecatedOrArchived || i.installScripts && (i.lowTrust || i.brandNew) || i.lowTrust && i.flags.includes("single-maintainer")) {
|
|
2870
|
+
return "medium";
|
|
2871
|
+
}
|
|
2872
|
+
return "low";
|
|
2873
|
+
}
|
|
2874
|
+
var init_risk = __esm({
|
|
2875
|
+
"src/security/risk.ts"() {
|
|
2876
|
+
"use strict";
|
|
2877
|
+
init_esm_shims();
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
// src/security/typosquat.ts
|
|
2882
|
+
function bareName(name) {
|
|
2883
|
+
const slash = name.indexOf("/");
|
|
2884
|
+
return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
|
|
2885
|
+
}
|
|
2886
|
+
function editDistance(a, b) {
|
|
2887
|
+
const m = a.length;
|
|
2888
|
+
const n = b.length;
|
|
2889
|
+
if (m === 0) return n;
|
|
2890
|
+
if (n === 0) return m;
|
|
2891
|
+
let prevPrev = new Array(n + 1).fill(0);
|
|
2892
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2893
|
+
for (let i = 1; i <= m; i++) {
|
|
2894
|
+
const cur = new Array(n + 1).fill(0);
|
|
2895
|
+
cur[0] = i;
|
|
2896
|
+
for (let j = 1; j <= n; j++) {
|
|
2897
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2898
|
+
let val = Math.min(
|
|
2899
|
+
(prev[j] ?? 0) + 1,
|
|
2900
|
+
(cur[j - 1] ?? 0) + 1,
|
|
2901
|
+
(prev[j - 1] ?? 0) + cost
|
|
2902
|
+
);
|
|
2903
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
2904
|
+
val = Math.min(val, (prevPrev[j - 2] ?? 0) + 1);
|
|
2905
|
+
}
|
|
2906
|
+
cur[j] = val;
|
|
2907
|
+
}
|
|
2908
|
+
prevPrev = prev;
|
|
2909
|
+
prev = cur;
|
|
2910
|
+
}
|
|
2911
|
+
return prev[n] ?? 0;
|
|
2912
|
+
}
|
|
2913
|
+
function detectTyposquat(name, popular, maxDistance = 2) {
|
|
2914
|
+
const target = bareName(name);
|
|
2915
|
+
if (target.length < 4) return null;
|
|
2916
|
+
if (popular.some((p) => p.toLowerCase() === name.toLowerCase())) return null;
|
|
2917
|
+
let best = null;
|
|
2918
|
+
for (const p of popular) {
|
|
2919
|
+
const cand = bareName(p);
|
|
2920
|
+
if (cand === target) continue;
|
|
2921
|
+
if (Math.abs(cand.length - target.length) > maxDistance) continue;
|
|
2922
|
+
const dist = editDistance(target, cand);
|
|
2923
|
+
if (dist >= 1 && dist <= maxDistance && (!best || dist < best.distance)) {
|
|
2924
|
+
best = { target: p, distance: dist };
|
|
2925
|
+
if (dist === 1) break;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return best;
|
|
2929
|
+
}
|
|
2930
|
+
var init_typosquat = __esm({
|
|
2931
|
+
"src/security/typosquat.ts"() {
|
|
2932
|
+
"use strict";
|
|
2933
|
+
init_esm_shims();
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2407
2937
|
// src/mcp/handlers.ts
|
|
2408
|
-
|
|
2938
|
+
var handlers_exports = {};
|
|
2939
|
+
__export(handlers_exports, {
|
|
2940
|
+
handleCompare: () => handleCompare,
|
|
2941
|
+
handleCompat: () => handleCompat,
|
|
2942
|
+
handleEvaluate: () => handleEvaluate,
|
|
2943
|
+
handleRecommend: () => handleRecommend,
|
|
2944
|
+
handleVerify: () => handleVerify,
|
|
2945
|
+
latestDataAsOf: () => latestDataAsOf,
|
|
2946
|
+
rowToEvaluate: () => rowToEvaluate
|
|
2947
|
+
});
|
|
2948
|
+
import { createHash as createHash3 } from "crypto";
|
|
2949
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2409
2950
|
function isStale(dataAsOf) {
|
|
2410
2951
|
if (!dataAsOf) return true;
|
|
2411
2952
|
return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
|
|
2412
2953
|
}
|
|
2954
|
+
function refreshStale(out) {
|
|
2955
|
+
out.stale = isStale(out.dataAsOf ? new Date(out.dataAsOf) : null) || void 0;
|
|
2956
|
+
return out;
|
|
2957
|
+
}
|
|
2413
2958
|
function withinDays(date, days) {
|
|
2414
2959
|
return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
|
|
2415
2960
|
}
|
|
@@ -2438,7 +2983,6 @@ function rowToEvaluate(row) {
|
|
|
2438
2983
|
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
2439
2984
|
weeklyDownloads: row.weeklyDownloads,
|
|
2440
2985
|
downloadGrowth90d: row.downloadGrowth90d,
|
|
2441
|
-
dependentsCount: row.dependentsCount,
|
|
2442
2986
|
scorecard: row.scorecard,
|
|
2443
2987
|
bundleMinGzipKb: row.bundleMinGzipKb,
|
|
2444
2988
|
deprecated: row.deprecated,
|
|
@@ -2450,40 +2994,83 @@ function rowToEvaluate(row) {
|
|
|
2450
2994
|
};
|
|
2451
2995
|
}
|
|
2452
2996
|
async function latestDataAsOf(db) {
|
|
2453
|
-
const [row] = await db.select({ m:
|
|
2997
|
+
const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
|
|
2454
2998
|
return new Date(row?.m ?? Date.now()).toISOString();
|
|
2455
2999
|
}
|
|
3000
|
+
function cacheKey(parts) {
|
|
3001
|
+
return createHash3("sha1").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
|
|
3002
|
+
}
|
|
2456
3003
|
async function handleRecommend(db, input) {
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3004
|
+
return cached2(
|
|
3005
|
+
"rec",
|
|
3006
|
+
cacheKey([input.need, input.category ?? null, input.constraints ?? null]),
|
|
3007
|
+
async () => {
|
|
3008
|
+
const candidates = await recommend(db, {
|
|
3009
|
+
need: input.need,
|
|
3010
|
+
category: input.category,
|
|
3011
|
+
constraints: input.constraints,
|
|
3012
|
+
limit: 5
|
|
3013
|
+
});
|
|
3014
|
+
return { dataAsOf: await latestDataAsOf(db), candidates };
|
|
3015
|
+
},
|
|
3016
|
+
// Don't cache empty results — the index may still be populating.
|
|
3017
|
+
{ skipCache: (r) => r.candidates.length === 0 }
|
|
3018
|
+
);
|
|
2464
3019
|
}
|
|
2465
3020
|
async function handleEvaluate(db, input) {
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
3021
|
+
const out = await cached2(
|
|
3022
|
+
"eval",
|
|
3023
|
+
cacheKey([input.package]),
|
|
3024
|
+
async () => {
|
|
3025
|
+
const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
|
|
3026
|
+
if (!row) {
|
|
3027
|
+
return {
|
|
3028
|
+
tracked: false,
|
|
3029
|
+
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.`
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
const evaluated = rowToEvaluate(row);
|
|
3033
|
+
const verification = await getLatestVerificationByName(db, row.name);
|
|
3034
|
+
return verification ? { ...evaluated, buildVerified: toBuildVerified(verification) } : evaluated;
|
|
3035
|
+
},
|
|
3036
|
+
// Don't cache "not found / not scored yet" — it may resolve on a later fetch.
|
|
3037
|
+
{ skipCache: (r) => "tracked" in r }
|
|
3038
|
+
);
|
|
3039
|
+
return "tracked" in out ? out : refreshStale(out);
|
|
2474
3040
|
}
|
|
2475
3041
|
async function handleCompare(db, input) {
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
3042
|
+
const out = await cached2(
|
|
3043
|
+
"cmp",
|
|
3044
|
+
cacheKey(input.packages),
|
|
3045
|
+
async () => {
|
|
3046
|
+
const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
|
|
3047
|
+
const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
|
|
3048
|
+
const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
|
|
3049
|
+
return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
|
|
3050
|
+
},
|
|
3051
|
+
// Don't cache a transient miss (a package that momentarily failed to fetch).
|
|
3052
|
+
{ skipCache: (r) => Boolean(r.missing?.length) }
|
|
2480
3053
|
);
|
|
2481
|
-
|
|
3054
|
+
out.rows = out.rows.map(refreshStale);
|
|
3055
|
+
return out;
|
|
3056
|
+
}
|
|
3057
|
+
function toBuildVerified(v) {
|
|
3058
|
+
return {
|
|
3059
|
+
version: v.version,
|
|
3060
|
+
installed: v.installed,
|
|
3061
|
+
loaded: v.imported,
|
|
3062
|
+
driver: v.driver,
|
|
3063
|
+
ranAt: v.ranAt ? v.ranAt.toISOString() : ""
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
async function handleCompat(db, input) {
|
|
3067
|
+
return checkCompat(db, input.packages);
|
|
2482
3068
|
}
|
|
2483
3069
|
async function handleVerify(db, input) {
|
|
2484
3070
|
const name = input.package;
|
|
2485
3071
|
const exists = await npmPackageExists(name);
|
|
2486
3072
|
if (!exists) {
|
|
3073
|
+
const typo2 = detectTyposquat(name, await getTopPackageNames(db).catch(() => []));
|
|
2487
3074
|
return {
|
|
2488
3075
|
exists: false,
|
|
2489
3076
|
tracked: false,
|
|
@@ -2491,25 +3078,46 @@ async function handleVerify(db, input) {
|
|
|
2491
3078
|
archived: false,
|
|
2492
3079
|
latestVersion: null,
|
|
2493
3080
|
weeklyDownloads: null,
|
|
2494
|
-
riskFlags: ["not-found-on-registry"],
|
|
3081
|
+
riskFlags: typo2 ? ["not-found-on-registry", `possible-typosquat-of:${typo2.target}`] : ["not-found-on-registry"],
|
|
3082
|
+
risk: "high",
|
|
3083
|
+
typosquatOf: typo2?.target ?? null,
|
|
2495
3084
|
confidence: null,
|
|
2496
3085
|
advisoryCount: 0
|
|
2497
3086
|
};
|
|
2498
3087
|
}
|
|
2499
|
-
const registry = await
|
|
2500
|
-
|
|
3088
|
+
const [registry, { row, wasTracked }, popular] = await Promise.all([
|
|
3089
|
+
fetchNpmRegistry(name).catch(() => null),
|
|
3090
|
+
getOrFetchPackage(db, name),
|
|
3091
|
+
getTopPackageNames(db).catch(() => [])
|
|
3092
|
+
]);
|
|
2501
3093
|
const weeklyDownloads = row?.weeklyDownloads ?? null;
|
|
2502
|
-
const
|
|
3094
|
+
const advisories = row?.advisories ?? [];
|
|
3095
|
+
const advisoryCount = advisories.length;
|
|
2503
3096
|
const deprecated = Boolean(row?.deprecated || registry?.deprecated);
|
|
2504
3097
|
const archived = Boolean(row?.archived);
|
|
3098
|
+
const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
|
|
3099
|
+
const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
|
|
3100
|
+
const installScripts = registry?.hasInstallScripts ?? false;
|
|
3101
|
+
const typo = detectTyposquat(name, popular);
|
|
2505
3102
|
const riskFlags = [];
|
|
3103
|
+
if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
|
|
2506
3104
|
if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
|
|
2507
3105
|
else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
|
|
2508
|
-
if (
|
|
3106
|
+
if (brandNew) riskFlags.push("published-within-7-days");
|
|
2509
3107
|
if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
|
|
3108
|
+
if (installScripts) riskFlags.push("runs-install-scripts");
|
|
2510
3109
|
if (advisoryCount > 0) riskFlags.push("has-known-advisory");
|
|
2511
3110
|
if (deprecated) riskFlags.push("deprecated");
|
|
2512
3111
|
if (archived) riskFlags.push("archived");
|
|
3112
|
+
const risk = assessRisk({
|
|
3113
|
+
flags: riskFlags,
|
|
3114
|
+
hasCriticalOrHighAdvisory: hasCriticalOrHighAdvisory(advisories),
|
|
3115
|
+
typosquat: Boolean(typo),
|
|
3116
|
+
installScripts,
|
|
3117
|
+
brandNew,
|
|
3118
|
+
lowTrust,
|
|
3119
|
+
deprecatedOrArchived: deprecated || archived
|
|
3120
|
+
});
|
|
2513
3121
|
return {
|
|
2514
3122
|
exists: true,
|
|
2515
3123
|
tracked: wasTracked,
|
|
@@ -2518,6 +3126,8 @@ async function handleVerify(db, input) {
|
|
|
2518
3126
|
latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
|
|
2519
3127
|
weeklyDownloads,
|
|
2520
3128
|
riskFlags,
|
|
3129
|
+
risk,
|
|
3130
|
+
typosquatOf: typo?.target ?? null,
|
|
2521
3131
|
confidence: row?.confidence ?? null,
|
|
2522
3132
|
advisoryCount
|
|
2523
3133
|
};
|
|
@@ -2527,12 +3137,19 @@ var init_handlers = __esm({
|
|
|
2527
3137
|
"src/mcp/handlers.ts"() {
|
|
2528
3138
|
"use strict";
|
|
2529
3139
|
init_esm_shims();
|
|
3140
|
+
init_cache();
|
|
2530
3141
|
init_constants();
|
|
3142
|
+
init_check();
|
|
3143
|
+
init_packages();
|
|
3144
|
+
init_verification();
|
|
2531
3145
|
init_schema();
|
|
2532
3146
|
init_sources();
|
|
2533
3147
|
init_summarize();
|
|
2534
3148
|
init_single();
|
|
3149
|
+
init_score();
|
|
2535
3150
|
init_recommend();
|
|
3151
|
+
init_risk();
|
|
3152
|
+
init_typosquat();
|
|
2536
3153
|
SEVERITY_RANK = {
|
|
2537
3154
|
critical: 4,
|
|
2538
3155
|
high: 3,
|
|
@@ -2545,7 +3162,7 @@ var init_handlers = __esm({
|
|
|
2545
3162
|
});
|
|
2546
3163
|
|
|
2547
3164
|
// src/mcp/diagram.ts
|
|
2548
|
-
import { inArray } from "drizzle-orm";
|
|
3165
|
+
import { inArray as inArray2 } from "drizzle-orm";
|
|
2549
3166
|
function layerFor(item) {
|
|
2550
3167
|
if (!item.category) return UNCLASSIFIED;
|
|
2551
3168
|
if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
|
|
@@ -2598,7 +3215,7 @@ async function handleDiagram(db, input) {
|
|
|
2598
3215
|
note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
|
|
2599
3216
|
};
|
|
2600
3217
|
}
|
|
2601
|
-
const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(
|
|
3218
|
+
const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray2(packages.name, input.stack));
|
|
2602
3219
|
const known = new Map(rows.map((r) => [r.name, r.category]));
|
|
2603
3220
|
const items = input.stack.map((name) => ({
|
|
2604
3221
|
label: name,
|
|
@@ -2652,13 +3269,410 @@ var init_diagram = __esm({
|
|
|
2652
3269
|
}
|
|
2653
3270
|
});
|
|
2654
3271
|
|
|
2655
|
-
// src/
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
3272
|
+
// src/compat/optimize.ts
|
|
3273
|
+
function conflictsFor(members, sandboxConflicts) {
|
|
3274
|
+
const out = resolveArchitectureCompat(members);
|
|
3275
|
+
for (let i = 0; i < members.length; i++) {
|
|
3276
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
3277
|
+
const a = members[i].name;
|
|
3278
|
+
const b = members[j].name;
|
|
3279
|
+
const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
|
|
3280
|
+
if (sandboxConflicts.has(key)) {
|
|
3281
|
+
out.push({
|
|
3282
|
+
source: "sandbox",
|
|
3283
|
+
packages: [a, b],
|
|
3284
|
+
detail: `${a} and ${b} are recorded as incompatible (sandbox)`
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return out;
|
|
3290
|
+
}
|
|
3291
|
+
function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
3292
|
+
if (slots.some((s) => s.length === 0)) {
|
|
3293
|
+
throw new Error("optimizeStack: every slot must have at least one candidate");
|
|
3294
|
+
}
|
|
3295
|
+
const n = slots.length;
|
|
3296
|
+
const budget = 5e4;
|
|
3297
|
+
let nodes = 0;
|
|
3298
|
+
let bestSelection = new Array(n).fill(0);
|
|
3299
|
+
let bestRegret = conflictsFor(
|
|
3300
|
+
slots.map((c) => c[0]).filter((m) => Boolean(m)),
|
|
3301
|
+
sandboxConflicts
|
|
3302
|
+
).length === 0 ? 0 : Infinity;
|
|
3303
|
+
const chosen = new Array(n).fill(0);
|
|
3304
|
+
const dfs = (slot, regret) => {
|
|
3305
|
+
if (nodes++ > budget || regret >= bestRegret) return;
|
|
3306
|
+
if (slot === n) {
|
|
3307
|
+
bestSelection = chosen.slice();
|
|
3308
|
+
bestRegret = regret;
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
const candidates = slots[slot];
|
|
3312
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
3313
|
+
chosen[slot] = i;
|
|
3314
|
+
const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
|
|
3315
|
+
if (conflictsFor(assigned, sandboxConflicts).length === 0) {
|
|
3316
|
+
dfs(slot + 1, regret + i);
|
|
3317
|
+
}
|
|
3318
|
+
if (nodes > budget) return;
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
dfs(0, 0);
|
|
3322
|
+
const members = bestSelection.map((idx, s) => slots[s][idx]);
|
|
3323
|
+
return {
|
|
3324
|
+
selection: bestSelection,
|
|
3325
|
+
conflicts: conflictsFor(members, sandboxConflicts),
|
|
3326
|
+
regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
var init_optimize = __esm({
|
|
3330
|
+
"src/compat/optimize.ts"() {
|
|
3331
|
+
"use strict";
|
|
3332
|
+
init_esm_shims();
|
|
3333
|
+
init_peerCompat();
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
// src/mcp/plan.ts
|
|
3338
|
+
var plan_exports = {};
|
|
3339
|
+
__export(plan_exports, {
|
|
3340
|
+
decomposeHeuristic: () => decomposeHeuristic,
|
|
3341
|
+
familyOf: () => familyOf,
|
|
3342
|
+
flagSlotConflicts: () => flagSlotConflicts,
|
|
3343
|
+
handlePlan: () => handlePlan,
|
|
3344
|
+
orderCandidates: () => orderCandidates,
|
|
3345
|
+
resolvePins: () => resolvePins
|
|
3346
|
+
});
|
|
3347
|
+
import { createHash as createHash4 } from "crypto";
|
|
3348
|
+
import { inArray as inArray3 } from "drizzle-orm";
|
|
3349
|
+
function packageToCandidate(row) {
|
|
3350
|
+
return {
|
|
3351
|
+
name: row.name,
|
|
3352
|
+
category: row.category,
|
|
3353
|
+
healthScore: row.healthScore ?? 0,
|
|
3354
|
+
qualityScore: row.qualityScore,
|
|
3355
|
+
confidence: row.confidence ?? "unproven",
|
|
3356
|
+
why: "pinned by you",
|
|
3357
|
+
latestVersion: row.latestVersion,
|
|
3358
|
+
weeklyDownloads: row.weeklyDownloads,
|
|
3359
|
+
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
3360
|
+
repoUrl: row.repoUrl
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
async function resolvePins(db, using, recommendedNames) {
|
|
3364
|
+
const slots = [];
|
|
3365
|
+
const unresolved = [];
|
|
3366
|
+
for (const name of new Set(using ?? [])) {
|
|
3367
|
+
if (recommendedNames.has(name)) continue;
|
|
3368
|
+
const { row } = await getOrFetchPackage(db, name);
|
|
3369
|
+
if (!row) {
|
|
3370
|
+
unresolved.push(name);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
slots.push({
|
|
3374
|
+
need: `using ${name}`,
|
|
3375
|
+
category: row.category,
|
|
3376
|
+
layer: layerFor({ label: name, category: row.category }),
|
|
3377
|
+
recommended: packageToCandidate(row),
|
|
3378
|
+
alternatives: [],
|
|
3379
|
+
// fixed: the user chose this, so it never gets swapped
|
|
3380
|
+
note: "pinned by you"
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
return { slots, unresolved };
|
|
3384
|
+
}
|
|
3385
|
+
async function handlePlan(db, input) {
|
|
3386
|
+
const optimize = input.optimize ?? "balanced";
|
|
3387
|
+
const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
|
|
3388
|
+
const hasPins = Boolean(input.using?.length);
|
|
3389
|
+
if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
|
|
3390
|
+
return {
|
|
3391
|
+
note: "Provide a `document` (a detailed description of your program), a `needs` array, or a `using` list of packages you have already chosen. lurq recommends evidence-scored packages per component \u2014 it does not invent an architecture from a bare prompt."
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
|
|
3395
|
+
const source = decomposed?.source ?? "needs";
|
|
3396
|
+
const safeRecommend = (need, category) => recommend(db, { need, category, limit: PER_SLOT }).catch((err) => {
|
|
3397
|
+
logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
|
|
3398
|
+
return [];
|
|
3399
|
+
});
|
|
3400
|
+
const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
|
|
3401
|
+
const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
|
|
3402
|
+
const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
|
|
3403
|
+
const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
|
|
3404
|
+
const recs = new Array(needs.length);
|
|
3405
|
+
let framework = anchorFamily;
|
|
3406
|
+
if (anchorIdx >= 0) {
|
|
3407
|
+
recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
|
|
3408
|
+
framework = recs[anchorIdx][0]?.name ?? anchorFamily;
|
|
3409
|
+
}
|
|
3410
|
+
const [, dataAsOf] = await Promise.all([
|
|
3411
|
+
Promise.all(
|
|
3412
|
+
needs.map(async (n, i) => {
|
|
3413
|
+
if (i === anchorIdx) return;
|
|
3414
|
+
const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
|
|
3415
|
+
recs[i] = await safeRecommend(need, effCat(n));
|
|
3416
|
+
})
|
|
3417
|
+
),
|
|
3418
|
+
latestDataAsOf(db)
|
|
3419
|
+
]);
|
|
3420
|
+
const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
|
|
3421
|
+
const recSlots = needs.map((n, i) => {
|
|
3422
|
+
const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
|
|
3423
|
+
const recommended = candidates[0] ?? null;
|
|
3424
|
+
const category = n.category ?? recommended?.category ?? inferCategory(n.need);
|
|
3425
|
+
return {
|
|
3426
|
+
need: n.need,
|
|
3427
|
+
category,
|
|
3428
|
+
layer: layerFor({ label: recommended?.name ?? n.need, category }),
|
|
3429
|
+
recommended,
|
|
3430
|
+
alternatives: candidates.slice(1),
|
|
3431
|
+
note: recommended ? void 0 : "no tracked package matched this need yet"
|
|
3432
|
+
};
|
|
3433
|
+
});
|
|
3434
|
+
const recommendedNames = new Set(
|
|
3435
|
+
recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
|
|
3436
|
+
);
|
|
3437
|
+
const { slots: pinnedSlots, unresolved: unresolvedPins } = await resolvePins(
|
|
3438
|
+
db,
|
|
3439
|
+
input.using,
|
|
3440
|
+
recommendedNames
|
|
3441
|
+
);
|
|
3442
|
+
const slots = [...pinnedSlots, ...recSlots];
|
|
3443
|
+
const compatibility = await resolveCompat(db, slots);
|
|
3444
|
+
const unmatched = [
|
|
3445
|
+
...slots.filter((s) => !s.recommended).map((s) => s.need),
|
|
3446
|
+
...unresolvedPins.map((n) => `${n} (pinned, but not found on npm)`)
|
|
3447
|
+
];
|
|
3448
|
+
const mermaid = buildMermaid(
|
|
3449
|
+
slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
|
|
3450
|
+
);
|
|
3451
|
+
return {
|
|
3452
|
+
dataAsOf,
|
|
3453
|
+
optimize,
|
|
3454
|
+
source,
|
|
3455
|
+
framework,
|
|
3456
|
+
slots,
|
|
3457
|
+
unmatched,
|
|
3458
|
+
mermaid,
|
|
3459
|
+
note: planNote(source, unmatched.length, optimize, framework),
|
|
3460
|
+
compatibility
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function flagSlotConflicts(picks, conflicts) {
|
|
3464
|
+
const out = /* @__PURE__ */ new Map();
|
|
3465
|
+
for (const c of conflicts) {
|
|
3466
|
+
for (const p of picks) {
|
|
3467
|
+
if (p.name && c.packages.includes(p.name)) {
|
|
3468
|
+
const others = c.packages.filter((n) => n !== p.name);
|
|
3469
|
+
out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
return out;
|
|
3474
|
+
}
|
|
3475
|
+
async function resolveCompat(db, slots) {
|
|
3476
|
+
const eligible = slots.filter((s) => s.recommended);
|
|
3477
|
+
if (eligible.length < 2) return null;
|
|
3478
|
+
try {
|
|
3479
|
+
const allNames = [
|
|
3480
|
+
...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
|
|
3481
|
+
];
|
|
3482
|
+
const [{ members }, edges] = await Promise.all([
|
|
3483
|
+
assembleMembers(db, allNames),
|
|
3484
|
+
getCompatEdges(db, allNames)
|
|
3485
|
+
]);
|
|
3486
|
+
const metaByName = new Map(members.map((m) => [m.name, m]));
|
|
3487
|
+
const sandboxConflicts = new Set(
|
|
3488
|
+
edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
|
|
3489
|
+
);
|
|
3490
|
+
const slotCandidates = eligible.map(
|
|
3491
|
+
(s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
|
|
3492
|
+
);
|
|
3493
|
+
const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
|
|
3494
|
+
eligible.forEach((s, i) => {
|
|
3495
|
+
const idx = selection[i] ?? 0;
|
|
3496
|
+
if (idx <= 0) return;
|
|
3497
|
+
const all = [s.recommended, ...s.alternatives];
|
|
3498
|
+
const chosen = all[idx];
|
|
3499
|
+
if (!chosen) return;
|
|
3500
|
+
s.recommended = chosen;
|
|
3501
|
+
s.alternatives = all.filter((c) => c.name !== chosen.name);
|
|
3502
|
+
s.swappedFrom = all[0].name;
|
|
3503
|
+
});
|
|
3504
|
+
} catch (err) {
|
|
3505
|
+
logger.warn(`plan: compat optimization failed: ${String(err)}`);
|
|
3506
|
+
}
|
|
3507
|
+
const compat = await checkCompat(
|
|
3508
|
+
db,
|
|
3509
|
+
eligible.map((s) => s.recommended.name)
|
|
3510
|
+
).catch(() => null);
|
|
3511
|
+
if (compat) {
|
|
3512
|
+
const flags = flagSlotConflicts(
|
|
3513
|
+
slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
|
|
3514
|
+
compat.conflicts
|
|
3515
|
+
);
|
|
3516
|
+
for (const s of slots) {
|
|
3517
|
+
const cw = flags.get(s.need);
|
|
3518
|
+
s.conflictsWith = cw?.length ? cw : void 0;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return compat;
|
|
3522
|
+
}
|
|
3523
|
+
function planNote(source, unmatched, optimize, framework) {
|
|
3524
|
+
const base = source === "heuristic" ? "Components were extracted from your document with a keyword heuristic (no summary LLM configured) \u2014 coarse; pass a `needs` array or set SUMMARY_API_KEY for sharper decomposition." : source === "llm" ? "Components were extracted from your document by the summary model." : "Components taken from the supplied needs.";
|
|
3525
|
+
const grounding = " Each package is recommended from lurq\u2019s scored index \u2014 a labeled, evidence-backed starting point, not a validated architecture.";
|
|
3526
|
+
const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
|
|
3527
|
+
const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
|
|
3528
|
+
const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
|
|
3529
|
+
return base + grounding + ctx + opt + tail;
|
|
3530
|
+
}
|
|
3531
|
+
function dedupeNeeds(needs) {
|
|
3532
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3533
|
+
for (const n of needs) {
|
|
3534
|
+
const key = n.need.trim().toLowerCase();
|
|
3535
|
+
if (!key) continue;
|
|
3536
|
+
if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
|
|
3537
|
+
}
|
|
3538
|
+
return [...seen.values()];
|
|
3539
|
+
}
|
|
3540
|
+
function familyOf(name) {
|
|
3541
|
+
const n = name.toLowerCase();
|
|
3542
|
+
for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
|
|
3546
|
+
const coherence = (c) => {
|
|
3547
|
+
const fam = familyOf(c.name);
|
|
3548
|
+
if (!fam) return 0;
|
|
3549
|
+
return fam === anchorFamily ? 1 : -1;
|
|
3550
|
+
};
|
|
3551
|
+
return [...cands].sort((a, b) => {
|
|
3552
|
+
if (anchorFamily) {
|
|
3553
|
+
const d = coherence(b) - coherence(a);
|
|
3554
|
+
if (d) return d;
|
|
3555
|
+
}
|
|
3556
|
+
if (optimize === "speed") {
|
|
3557
|
+
return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
|
|
3558
|
+
}
|
|
3559
|
+
return 0;
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
async function bundleSizes(db, candidates) {
|
|
3563
|
+
const names = [...new Set(candidates.map((c) => c.name))];
|
|
3564
|
+
if (names.length === 0) return /* @__PURE__ */ new Map();
|
|
3565
|
+
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray3(packages.name, names));
|
|
3566
|
+
return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
|
|
3567
|
+
}
|
|
3568
|
+
async function decompose(document) {
|
|
3569
|
+
const config = getConfig();
|
|
3570
|
+
if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
|
|
3571
|
+
const llm = await decomposeWithLlm(document, config.SUMMARY_API_KEY, config.SUMMARY_MODEL).catch(
|
|
3572
|
+
(err) => {
|
|
3573
|
+
logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
|
|
3574
|
+
return null;
|
|
3575
|
+
}
|
|
3576
|
+
);
|
|
3577
|
+
if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
|
|
3578
|
+
}
|
|
3579
|
+
return { needs: decomposeHeuristic(document), source: "heuristic" };
|
|
3580
|
+
}
|
|
3581
|
+
async function decomposeWithLlm(document, apiKey, model) {
|
|
3582
|
+
const prompt = [
|
|
3583
|
+
"Project description:",
|
|
3584
|
+
document.slice(0, 8e3),
|
|
3585
|
+
"",
|
|
3586
|
+
'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
|
|
3587
|
+
"One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
|
|
3588
|
+
].join("\n");
|
|
3589
|
+
const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
|
|
3590
|
+
host: "api.openai.com",
|
|
3591
|
+
method: "POST",
|
|
3592
|
+
ttlMs: 24 * 60 * 60 * 1e3,
|
|
3593
|
+
// Hash the FULL document — length + a 64-char prefix collide for same-length
|
|
3594
|
+
// edits or shared templated headers, which would serve a stale decomposition.
|
|
3595
|
+
cacheKey: `openai-plan ${model} ${createHash4("sha1").update(document).digest("hex")}`,
|
|
3596
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
3597
|
+
body: JSON.stringify({
|
|
3598
|
+
model,
|
|
3599
|
+
messages: [
|
|
3600
|
+
{ role: "system", content: DECOMPOSE_SYSTEM },
|
|
3601
|
+
{ role: "user", content: prompt }
|
|
3602
|
+
],
|
|
3603
|
+
response_format: { type: "json_object" },
|
|
3604
|
+
temperature: 0.2
|
|
3605
|
+
})
|
|
3606
|
+
});
|
|
3607
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
3608
|
+
const parsed = content ? JSON.parse(content) : {};
|
|
3609
|
+
const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
|
|
3610
|
+
return raw.map((n) => {
|
|
3611
|
+
const need = typeof n?.need === "string" ? n.need.trim() : "";
|
|
3612
|
+
if (!need) return null;
|
|
3613
|
+
const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
|
|
3614
|
+
return { need, category: cat };
|
|
3615
|
+
}).filter(Boolean);
|
|
3616
|
+
}
|
|
3617
|
+
function decomposeHeuristic(document) {
|
|
3618
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
3619
|
+
for (const rawLine of document.split("\n")) {
|
|
3620
|
+
const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
|
|
3621
|
+
if (line.length < 3) continue;
|
|
3622
|
+
const category = inferCategory(line);
|
|
3623
|
+
if (category && !byCategory.has(category)) {
|
|
3624
|
+
byCategory.set(category, line.slice(0, 120));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
|
|
3628
|
+
}
|
|
3629
|
+
var PER_SLOT, MAX_SLOTS, NO_META, FAMILY_TOKENS, DECOMPOSE_SYSTEM;
|
|
3630
|
+
var init_plan = __esm({
|
|
3631
|
+
"src/mcp/plan.ts"() {
|
|
3632
|
+
"use strict";
|
|
3633
|
+
init_esm_shims();
|
|
3634
|
+
init_config();
|
|
3635
|
+
init_http();
|
|
3636
|
+
init_check();
|
|
3637
|
+
init_members();
|
|
3638
|
+
init_optimize();
|
|
3639
|
+
init_compat();
|
|
3640
|
+
init_logger();
|
|
3641
|
+
init_types();
|
|
3642
|
+
init_schema();
|
|
3643
|
+
init_categoryInference();
|
|
3644
|
+
init_recommend();
|
|
3645
|
+
init_single();
|
|
3646
|
+
init_diagram();
|
|
3647
|
+
init_handlers();
|
|
3648
|
+
PER_SLOT = 3;
|
|
3649
|
+
MAX_SLOTS = 24;
|
|
3650
|
+
NO_META = (c) => ({
|
|
3651
|
+
name: c.name,
|
|
3652
|
+
version: c.latestVersion,
|
|
3653
|
+
peerDependencies: null,
|
|
3654
|
+
peerDependenciesMeta: null,
|
|
3655
|
+
engines: null
|
|
3656
|
+
});
|
|
3657
|
+
FAMILY_TOKENS = [
|
|
3658
|
+
{ family: "react", re: /\breact\b|preact/ },
|
|
3659
|
+
{ family: "vue", re: /\bvue\b|nuxt/ },
|
|
3660
|
+
{ family: "angular", re: /angular/ },
|
|
3661
|
+
{ family: "svelte", re: /svelte/ },
|
|
3662
|
+
{ family: "solid", re: /\bsolid(-?js)?\b/ }
|
|
3663
|
+
];
|
|
3664
|
+
DECOMPOSE_SYSTEM = "You break a software project description into the distinct technical components that each need a library. Return ONLY components the project actually requires, grounded in the description. Respond with a JSON object.";
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
|
|
3668
|
+
// src/mcp/server.ts
|
|
3669
|
+
var server_exports = {};
|
|
3670
|
+
__export(server_exports, {
|
|
3671
|
+
buildMcpServer: () => buildMcpServer,
|
|
3672
|
+
npmName: () => npmName,
|
|
3673
|
+
startMcpServer: () => startMcpServer
|
|
3674
|
+
});
|
|
3675
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2662
3676
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2663
3677
|
import { z as z2 } from "zod";
|
|
2664
3678
|
function json(obj) {
|
|
@@ -2685,7 +3699,7 @@ function buildMcpServer(db) {
|
|
|
2685
3699
|
title: "Evaluate a package",
|
|
2686
3700
|
description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
|
|
2687
3701
|
inputSchema: {
|
|
2688
|
-
package:
|
|
3702
|
+
package: npmName.describe("npm package name")
|
|
2689
3703
|
}
|
|
2690
3704
|
},
|
|
2691
3705
|
async (args) => json(await handleEvaluate(db, args))
|
|
@@ -2696,18 +3710,29 @@ function buildMcpServer(db) {
|
|
|
2696
3710
|
title: "Compare packages",
|
|
2697
3711
|
description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
|
|
2698
3712
|
inputSchema: {
|
|
2699
|
-
packages: z2.array(
|
|
3713
|
+
packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
|
|
2700
3714
|
}
|
|
2701
3715
|
},
|
|
2702
3716
|
async (args) => json(await handleCompare(db, args))
|
|
2703
3717
|
);
|
|
3718
|
+
server.registerTool(
|
|
3719
|
+
"compat",
|
|
3720
|
+
{
|
|
3721
|
+
title: "Check package compatibility",
|
|
3722
|
+
description: "Check whether a set of packages forms a coherent stack: peer-dependency and engine-range compatibility across the whole set (instant, from declared metadata), plus any recorded sandbox-verified conflicts. Returns the exact clashing constraints. Read-only \u2014 does not run installs. Call before committing to a multi-package stack.",
|
|
3723
|
+
inputSchema: {
|
|
3724
|
+
packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
|
|
3725
|
+
}
|
|
3726
|
+
},
|
|
3727
|
+
async (args) => json(await handleCompat(db, args))
|
|
3728
|
+
);
|
|
2704
3729
|
server.registerTool(
|
|
2705
3730
|
"verify",
|
|
2706
3731
|
{
|
|
2707
3732
|
title: "Verify a package",
|
|
2708
3733
|
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.",
|
|
2709
3734
|
inputSchema: {
|
|
2710
|
-
package:
|
|
3735
|
+
package: npmName.describe("npm package name to verify")
|
|
2711
3736
|
}
|
|
2712
3737
|
},
|
|
2713
3738
|
async (args) => json(await handleVerify(db, args))
|
|
@@ -2718,11 +3743,32 @@ function buildMcpServer(db) {
|
|
|
2718
3743
|
title: "Reference architecture diagram",
|
|
2719
3744
|
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.",
|
|
2720
3745
|
inputSchema: {
|
|
2721
|
-
stack: z2.array(
|
|
3746
|
+
stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
|
|
2722
3747
|
}
|
|
2723
3748
|
},
|
|
2724
3749
|
async (args) => json(await handleDiagram(db, args))
|
|
2725
3750
|
);
|
|
3751
|
+
server.registerTool(
|
|
3752
|
+
"plan",
|
|
3753
|
+
{
|
|
3754
|
+
title: "Plan a stack from a program description",
|
|
3755
|
+
description: "Turn a detailed program description (spec/README) or a list of component needs into an evidence-scored build plan: a real, lurq-scored package recommended per component, plus a Mermaid roadmap other agents can parse. Recommends building blocks slot-by-slot from the index \u2014 it does not invent an architecture from a bare prompt.",
|
|
3756
|
+
inputSchema: {
|
|
3757
|
+
document: z2.string().optional().describe("Detailed description of the program (spec/README); lurq decomposes it into components"),
|
|
3758
|
+
needs: z2.array(
|
|
3759
|
+
z2.object({
|
|
3760
|
+
need: z2.string().min(1).describe("A component that needs a library"),
|
|
3761
|
+
category: categoryEnum.optional()
|
|
3762
|
+
})
|
|
3763
|
+
).optional().describe("Pre-decomposed components (skip if you pass a document)"),
|
|
3764
|
+
using: z2.array(npmName).max(12).optional().describe(
|
|
3765
|
+
"Packages you have already decided on. lurq pins these as fixed slots, recommends only the remaining needs, and checks/optimizes the whole stack around your picks."
|
|
3766
|
+
),
|
|
3767
|
+
optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
|
|
3768
|
+
}
|
|
3769
|
+
},
|
|
3770
|
+
async (args) => json(await handlePlan(db, args))
|
|
3771
|
+
);
|
|
2726
3772
|
return server;
|
|
2727
3773
|
}
|
|
2728
3774
|
async function startMcpServer() {
|
|
@@ -2741,7 +3787,7 @@ async function startMcpServer() {
|
|
|
2741
3787
|
await server.connect(transport);
|
|
2742
3788
|
logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
|
|
2743
3789
|
}
|
|
2744
|
-
var categoryEnum, confidenceEnum, constraintsSchema;
|
|
3790
|
+
var categoryEnum, confidenceEnum, npmName, constraintsSchema;
|
|
2745
3791
|
var init_server = __esm({
|
|
2746
3792
|
"src/mcp/server.ts"() {
|
|
2747
3793
|
"use strict";
|
|
@@ -2752,8 +3798,10 @@ var init_server = __esm({
|
|
|
2752
3798
|
init_logger();
|
|
2753
3799
|
init_handlers();
|
|
2754
3800
|
init_diagram();
|
|
3801
|
+
init_plan();
|
|
2755
3802
|
categoryEnum = z2.enum(CATEGORIES);
|
|
2756
3803
|
confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
|
|
3804
|
+
npmName = z2.string().trim().min(1).max(214).regex(/^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/i, "Invalid npm package name");
|
|
2757
3805
|
constraintsSchema = z2.object({
|
|
2758
3806
|
runtime: z2.enum(["browser", "node", "both"]).optional(),
|
|
2759
3807
|
license: z2.string().optional(),
|
|
@@ -2764,10 +3812,10 @@ var init_server = __esm({
|
|
|
2764
3812
|
});
|
|
2765
3813
|
|
|
2766
3814
|
// src/auth/apiKeys.ts
|
|
2767
|
-
import { createHash as
|
|
2768
|
-
import { and as
|
|
3815
|
+
import { createHash as createHash5, randomBytes } from "crypto";
|
|
3816
|
+
import { and as and5, desc as desc3, eq as eq5, isNull } from "drizzle-orm";
|
|
2769
3817
|
function hashKey(key) {
|
|
2770
|
-
return
|
|
3818
|
+
return createHash5("sha256").update(key).digest("hex");
|
|
2771
3819
|
}
|
|
2772
3820
|
function generateApiKey() {
|
|
2773
3821
|
const body = randomBytes(24).toString("base64url");
|
|
@@ -2787,21 +3835,34 @@ async function createKey(db, input = {}) {
|
|
|
2787
3835
|
}
|
|
2788
3836
|
async function lookupActiveKey(db, key) {
|
|
2789
3837
|
const hash = hashKey(key);
|
|
2790
|
-
const [row] = await db.select().from(apiKeys).where(
|
|
3838
|
+
const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
|
|
2791
3839
|
if (!row) return null;
|
|
2792
|
-
db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
3840
|
+
db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, row.id)).then(void 0, () => {
|
|
2793
3841
|
});
|
|
2794
3842
|
return row;
|
|
2795
3843
|
}
|
|
2796
3844
|
async function listKeys(db) {
|
|
2797
|
-
return db.select().from(apiKeys).orderBy(
|
|
3845
|
+
return db.select().from(apiKeys).orderBy(desc3(apiKeys.createdAt));
|
|
2798
3846
|
}
|
|
2799
|
-
|
|
3847
|
+
function matchByPrefixOrId(prefixOrId) {
|
|
2800
3848
|
const asId = Number(prefixOrId);
|
|
2801
|
-
|
|
2802
|
-
|
|
3849
|
+
return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq5(apiKeys.id, asId) : eq5(apiKeys.prefix, prefixOrId);
|
|
3850
|
+
}
|
|
3851
|
+
async function revokeKey(db, prefixOrId) {
|
|
3852
|
+
const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
|
|
2803
3853
|
return rows.length;
|
|
2804
3854
|
}
|
|
3855
|
+
async function rotateKey(db, prefixOrId) {
|
|
3856
|
+
const [previous] = await db.select().from(apiKeys).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).limit(1);
|
|
3857
|
+
if (!previous) return null;
|
|
3858
|
+
const { key, row } = await createKey(db, {
|
|
3859
|
+
label: previous.label ?? void 0,
|
|
3860
|
+
tier: previous.tier,
|
|
3861
|
+
ownerId: previous.ownerId ?? void 0
|
|
3862
|
+
});
|
|
3863
|
+
await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
|
|
3864
|
+
return { key, row, previous };
|
|
3865
|
+
}
|
|
2805
3866
|
var DISPLAY_BODY;
|
|
2806
3867
|
var init_apiKeys = __esm({
|
|
2807
3868
|
"src/auth/apiKeys.ts"() {
|
|
@@ -2866,10 +3927,15 @@ async function startHttpServer(opts = {}) {
|
|
|
2866
3927
|
limit: config.LURQ_RATE_LIMIT_MAX,
|
|
2867
3928
|
standardHeaders: "draft-7",
|
|
2868
3929
|
legacyHeaders: false,
|
|
2869
|
-
// Key on the resolved API key (always present — auth runs
|
|
2870
|
-
//
|
|
2871
|
-
//
|
|
2872
|
-
|
|
3930
|
+
// Key on the resolved API key's unique row id (always present — auth runs
|
|
3931
|
+
// first). The display `prefix` is only 6 chars of body, so distinct keys
|
|
3932
|
+
// can collide on it and share a quota; the id cannot. The IP fallback uses
|
|
3933
|
+
// express-rate-limit's ipKeyGenerator so IPv6 addresses are normalized
|
|
3934
|
+
// correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
|
|
3935
|
+
keyGenerator: (req) => {
|
|
3936
|
+
const id = req.lurqKey?.id;
|
|
3937
|
+
return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
|
|
3938
|
+
},
|
|
2873
3939
|
message: rpcError(-32029, "Rate limit exceeded.")
|
|
2874
3940
|
});
|
|
2875
3941
|
app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
|
|
@@ -2910,23 +3976,24 @@ var init_http2 = __esm({
|
|
|
2910
3976
|
});
|
|
2911
3977
|
|
|
2912
3978
|
// src/pipeline/rescore.ts
|
|
2913
|
-
import { isNotNull as
|
|
2914
|
-
import { eq as
|
|
3979
|
+
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
3980
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
2915
3981
|
async function runRescore() {
|
|
2916
3982
|
const weights = loadWeights();
|
|
2917
3983
|
const handle = createDb({ max: 4 });
|
|
2918
3984
|
try {
|
|
2919
|
-
const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(
|
|
3985
|
+
const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull4(packages.scoreBreakdown));
|
|
2920
3986
|
let updated = 0;
|
|
2921
3987
|
for (const row of rows) {
|
|
2922
3988
|
if (!row.breakdown) continue;
|
|
2923
3989
|
const health = computeHealthScore(row.breakdown, weights.health);
|
|
2924
3990
|
if (health !== row.healthScore) {
|
|
2925
|
-
await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(
|
|
3991
|
+
await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq6(packages.id, row.id));
|
|
2926
3992
|
updated++;
|
|
2927
3993
|
}
|
|
2928
3994
|
}
|
|
2929
3995
|
logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
|
|
3996
|
+
if (updated > 0) await invalidateCache();
|
|
2930
3997
|
return { seen: rows.length, updated };
|
|
2931
3998
|
} finally {
|
|
2932
3999
|
await handle.close();
|
|
@@ -2936,6 +4003,7 @@ var init_rescore = __esm({
|
|
|
2936
4003
|
"src/pipeline/rescore.ts"() {
|
|
2937
4004
|
"use strict";
|
|
2938
4005
|
init_esm_shims();
|
|
4006
|
+
init_cache();
|
|
2939
4007
|
init_logger();
|
|
2940
4008
|
init_client();
|
|
2941
4009
|
init_schema();
|
|
@@ -2945,7 +4013,7 @@ var init_rescore = __esm({
|
|
|
2945
4013
|
});
|
|
2946
4014
|
|
|
2947
4015
|
// src/db/discovery.ts
|
|
2948
|
-
import { eq as
|
|
4016
|
+
import { eq as eq7 } from "drizzle-orm";
|
|
2949
4017
|
async function getKnownNames(db) {
|
|
2950
4018
|
const [tracked, queued] = await Promise.all([
|
|
2951
4019
|
db.select({ name: packages.name }).from(packages),
|
|
@@ -2960,10 +4028,10 @@ async function enqueueCandidates(db, candidates) {
|
|
|
2960
4028
|
return inserted.length;
|
|
2961
4029
|
}
|
|
2962
4030
|
async function getPendingCandidates(db, limit) {
|
|
2963
|
-
return db.select().from(discoveryQueue).where(
|
|
4031
|
+
return db.select().from(discoveryQueue).where(eq7(discoveryQueue.status, "pending")).limit(limit);
|
|
2964
4032
|
}
|
|
2965
4033
|
async function setDiscoveryStatus(db, name, data) {
|
|
2966
|
-
await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(
|
|
4034
|
+
await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq7(discoveryQueue.name, name));
|
|
2967
4035
|
}
|
|
2968
4036
|
var init_discovery = __esm({
|
|
2969
4037
|
"src/db/discovery.ts"() {
|
|
@@ -2974,7 +4042,7 @@ var init_discovery = __esm({
|
|
|
2974
4042
|
});
|
|
2975
4043
|
|
|
2976
4044
|
// src/pipeline/discovery.ts
|
|
2977
|
-
import { isNotNull as
|
|
4045
|
+
import { isNotNull as isNotNull5 } from "drizzle-orm";
|
|
2978
4046
|
function selectCandidates(raw, known) {
|
|
2979
4047
|
const seen = new Set(known);
|
|
2980
4048
|
const out = [];
|
|
@@ -3007,7 +4075,7 @@ async function preScorePackage(name, fetchImpl) {
|
|
|
3007
4075
|
}
|
|
3008
4076
|
}
|
|
3009
4077
|
async function graphChannel(db) {
|
|
3010
|
-
const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(
|
|
4078
|
+
const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull5(packages.latestVersion));
|
|
3011
4079
|
const out = [];
|
|
3012
4080
|
for (const t of tracked) {
|
|
3013
4081
|
if (!t.version) continue;
|
|
@@ -3177,14 +4245,460 @@ var init_format = __esm({
|
|
|
3177
4245
|
}
|
|
3178
4246
|
});
|
|
3179
4247
|
|
|
4248
|
+
// src/cli/planView.ts
|
|
4249
|
+
var planView_exports = {};
|
|
4250
|
+
__export(planView_exports, {
|
|
4251
|
+
renderPlanHtml: () => renderPlanHtml
|
|
4252
|
+
});
|
|
4253
|
+
function esc(s) {
|
|
4254
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4255
|
+
}
|
|
4256
|
+
function slotRows(plan) {
|
|
4257
|
+
return plan.slots.map((s) => {
|
|
4258
|
+
const rec = s.recommended;
|
|
4259
|
+
const name = rec ? rec.repoUrl ? `<a href="${esc(rec.repoUrl)}" target="_blank" rel="noreferrer">${esc(rec.name)}</a>` : esc(rec.name) : '<span class="muted">\u2014 no match \u2014</span>';
|
|
4260
|
+
const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">\u2014</span>';
|
|
4261
|
+
return `<tr>
|
|
4262
|
+
<td>${esc(s.need)}</td>
|
|
4263
|
+
<td><span class="layer">${esc(s.layer)}</span></td>
|
|
4264
|
+
<td class="pkg">${name}</td>
|
|
4265
|
+
<td class="num">${rec ? rec.healthScore : "\u2014"}</td>
|
|
4266
|
+
<td>${rec ? esc(rec.confidence) : "\u2014"}</td>
|
|
4267
|
+
<td class="alts">${alts}</td>
|
|
4268
|
+
</tr>`;
|
|
4269
|
+
}).join("\n");
|
|
4270
|
+
}
|
|
4271
|
+
function renderPlanHtml(plan) {
|
|
4272
|
+
return `<!doctype html>
|
|
4273
|
+
<html lang="en">
|
|
4274
|
+
<head>
|
|
4275
|
+
<meta charset="utf-8" />
|
|
4276
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4277
|
+
<title>lurq plan \u2014 roadmap</title>
|
|
4278
|
+
<style>
|
|
4279
|
+
:root { color-scheme: dark; }
|
|
4280
|
+
* { box-sizing: border-box; }
|
|
4281
|
+
body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
4282
|
+
background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
|
|
4283
|
+
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
4284
|
+
h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
|
|
4285
|
+
.sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
|
|
4286
|
+
.diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
|
|
4287
|
+
table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
|
|
4288
|
+
th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
|
|
4289
|
+
th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
|
|
4290
|
+
.pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
|
|
4291
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
4292
|
+
.muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
|
|
4293
|
+
.layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
|
|
4294
|
+
footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
|
|
4295
|
+
</style>
|
|
4296
|
+
</head>
|
|
4297
|
+
<body>
|
|
4298
|
+
<h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
|
|
4299
|
+
<p class="sub">${esc(plan.note)}</p>
|
|
4300
|
+
|
|
4301
|
+
<h2>Roadmap</h2>
|
|
4302
|
+
<div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
|
|
4303
|
+
|
|
4304
|
+
<h2>Components</h2>
|
|
4305
|
+
<table>
|
|
4306
|
+
<thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
|
|
4307
|
+
<tbody>
|
|
4308
|
+
${slotRows(plan)}
|
|
4309
|
+
</tbody>
|
|
4310
|
+
</table>
|
|
4311
|
+
|
|
4312
|
+
<footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
|
|
4313
|
+
|
|
4314
|
+
<script type="module">
|
|
4315
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
4316
|
+
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
|
4317
|
+
</script>
|
|
4318
|
+
</body>
|
|
4319
|
+
</html>
|
|
4320
|
+
`;
|
|
4321
|
+
}
|
|
4322
|
+
var init_planView = __esm({
|
|
4323
|
+
"src/cli/planView.ts"() {
|
|
4324
|
+
"use strict";
|
|
4325
|
+
init_esm_shims();
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
// src/db/watch.ts
|
|
4330
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
4331
|
+
async function getWatchCursor(db, id) {
|
|
4332
|
+
const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq8(watchState.id, id)).limit(1);
|
|
4333
|
+
return rows[0]?.seq ?? null;
|
|
4334
|
+
}
|
|
4335
|
+
async function setWatchCursor(db, id, seq) {
|
|
4336
|
+
await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
|
|
4337
|
+
}
|
|
4338
|
+
var init_watch = __esm({
|
|
4339
|
+
"src/db/watch.ts"() {
|
|
4340
|
+
"use strict";
|
|
4341
|
+
init_esm_shims();
|
|
4342
|
+
init_schema();
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
|
|
4346
|
+
// src/pipeline/watch.ts
|
|
4347
|
+
var watch_exports = {};
|
|
4348
|
+
__export(watch_exports, {
|
|
4349
|
+
parseChangeLine: () => parseChangeLine,
|
|
4350
|
+
watchNpmChanges: () => watchNpmChanges
|
|
4351
|
+
});
|
|
4352
|
+
function parseChangeLine(line) {
|
|
4353
|
+
const trimmed = line.trim();
|
|
4354
|
+
if (!trimmed) return null;
|
|
4355
|
+
try {
|
|
4356
|
+
const obj = JSON.parse(trimmed);
|
|
4357
|
+
if (typeof obj?.id !== "string" || obj.seq == null) return null;
|
|
4358
|
+
return { seq: obj.seq, id: obj.id, deleted: Boolean(obj.deleted) };
|
|
4359
|
+
} catch {
|
|
4360
|
+
return null;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
async function watchNpmChanges(db, opts = {}) {
|
|
4364
|
+
const { signal } = opts;
|
|
4365
|
+
let backoff2 = 1e3;
|
|
4366
|
+
while (!signal?.aborted) {
|
|
4367
|
+
let tracked = new Set(await getAllPackageNames(db));
|
|
4368
|
+
let trackedAt = Date.now();
|
|
4369
|
+
const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
|
|
4370
|
+
const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
|
|
4371
|
+
logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
|
|
4372
|
+
try {
|
|
4373
|
+
const res = await fetch(url, { signal });
|
|
4374
|
+
if (!res.ok || !res.body) throw new Error(`feed responded ${res.status}`);
|
|
4375
|
+
backoff2 = 1e3;
|
|
4376
|
+
let sinceCheckpoint = 0;
|
|
4377
|
+
for await (const line of ndjsonLines(res.body, signal)) {
|
|
4378
|
+
const change = parseChangeLine(line);
|
|
4379
|
+
if (!change) continue;
|
|
4380
|
+
const seq = String(change.seq);
|
|
4381
|
+
sinceCheckpoint++;
|
|
4382
|
+
if (Date.now() - trackedAt > TRACKED_REFRESH_MS) {
|
|
4383
|
+
tracked = new Set(await getAllPackageNames(db));
|
|
4384
|
+
trackedAt = Date.now();
|
|
4385
|
+
}
|
|
4386
|
+
if (!change.deleted && tracked.has(change.id)) {
|
|
4387
|
+
logger.info(`watch: re-syncing ${change.id} (seq=${seq})`);
|
|
4388
|
+
await syncOnePackage(db, change.id).catch(
|
|
4389
|
+
(err) => logger.warn(`watch: re-sync failed for ${change.id}: ${String(err)}`)
|
|
4390
|
+
);
|
|
4391
|
+
await setWatchCursor(db, FEED_ID, seq);
|
|
4392
|
+
sinceCheckpoint = 0;
|
|
4393
|
+
} else if (sinceCheckpoint >= CHECKPOINT_EVERY) {
|
|
4394
|
+
await setWatchCursor(db, FEED_ID, seq);
|
|
4395
|
+
sinceCheckpoint = 0;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
logger.info("watch: feed stream ended; reconnecting");
|
|
4399
|
+
} catch (err) {
|
|
4400
|
+
if (signal?.aborted) break;
|
|
4401
|
+
logger.warn(`watch: ${String(err)} \u2014 retrying in ${backoff2}ms`);
|
|
4402
|
+
await sleep(backoff2, signal);
|
|
4403
|
+
backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
async function* ndjsonLines(body, signal) {
|
|
4408
|
+
const reader = body.getReader();
|
|
4409
|
+
const decoder = new TextDecoder();
|
|
4410
|
+
let buffer = "";
|
|
4411
|
+
try {
|
|
4412
|
+
while (!signal?.aborted) {
|
|
4413
|
+
const { value, done } = await reader.read();
|
|
4414
|
+
if (done) break;
|
|
4415
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4416
|
+
let nl;
|
|
4417
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
4418
|
+
yield buffer.slice(0, nl);
|
|
4419
|
+
buffer = buffer.slice(nl + 1);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
} finally {
|
|
4423
|
+
reader.releaseLock();
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
function sleep(ms, signal) {
|
|
4427
|
+
return new Promise((resolve) => {
|
|
4428
|
+
const timer = setTimeout(resolve, ms);
|
|
4429
|
+
signal?.addEventListener(
|
|
4430
|
+
"abort",
|
|
4431
|
+
() => {
|
|
4432
|
+
clearTimeout(timer);
|
|
4433
|
+
resolve();
|
|
4434
|
+
},
|
|
4435
|
+
{ once: true }
|
|
4436
|
+
);
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
var FEED_ID, FEED_URL, HEARTBEAT_MS, TRACKED_REFRESH_MS, CHECKPOINT_EVERY, MAX_BACKOFF_MS;
|
|
4440
|
+
var init_watch2 = __esm({
|
|
4441
|
+
"src/pipeline/watch.ts"() {
|
|
4442
|
+
"use strict";
|
|
4443
|
+
init_esm_shims();
|
|
4444
|
+
init_logger();
|
|
4445
|
+
init_packages();
|
|
4446
|
+
init_watch();
|
|
4447
|
+
init_single();
|
|
4448
|
+
FEED_ID = "npm-changes";
|
|
4449
|
+
FEED_URL = "https://replicate.npmjs.com/_changes";
|
|
4450
|
+
HEARTBEAT_MS = 3e4;
|
|
4451
|
+
TRACKED_REFRESH_MS = 5 * 6e4;
|
|
4452
|
+
CHECKPOINT_EVERY = 200;
|
|
4453
|
+
MAX_BACKOFF_MS = 3e4;
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
// src/sandbox/types.ts
|
|
4458
|
+
var DEFAULT_TARGET;
|
|
4459
|
+
var init_types2 = __esm({
|
|
4460
|
+
"src/sandbox/types.ts"() {
|
|
4461
|
+
"use strict";
|
|
4462
|
+
init_esm_shims();
|
|
4463
|
+
DEFAULT_TARGET = {
|
|
4464
|
+
node: "20",
|
|
4465
|
+
moduleSystem: "cjs"
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
});
|
|
4469
|
+
|
|
4470
|
+
// src/sandbox/local.ts
|
|
4471
|
+
import { execFile } from "child_process";
|
|
4472
|
+
import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
4473
|
+
import { tmpdir } from "os";
|
|
4474
|
+
import { join as join3 } from "path";
|
|
4475
|
+
import { promisify } from "util";
|
|
4476
|
+
function npmInstallArgs(specs, opts) {
|
|
4477
|
+
const bad = specs.find((s) => s.startsWith("-"));
|
|
4478
|
+
if (bad) throw new Error(`Invalid package spec: ${bad}`);
|
|
4479
|
+
const args = ["install", ...specs, "--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
|
|
4480
|
+
if (!opts.allowScripts) args.push("--ignore-scripts");
|
|
4481
|
+
return args;
|
|
4482
|
+
}
|
|
4483
|
+
function smokeScript(pkg, moduleSystem) {
|
|
4484
|
+
return moduleSystem === "esm" ? ["--input-type=module", "-e", `await import(${JSON.stringify(pkg)})`] : ["-e", `require(${JSON.stringify(pkg)})`];
|
|
4485
|
+
}
|
|
4486
|
+
function condense(s) {
|
|
4487
|
+
return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX);
|
|
4488
|
+
}
|
|
4489
|
+
function stderrOf(err) {
|
|
4490
|
+
if (err && typeof err === "object") {
|
|
4491
|
+
const e = err;
|
|
4492
|
+
if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
|
|
4493
|
+
if (typeof e.message === "string") return e.message;
|
|
4494
|
+
}
|
|
4495
|
+
return String(err);
|
|
4496
|
+
}
|
|
4497
|
+
var execFileAsync, INSTALL_TIMEOUT_MS, SMOKE_TIMEOUT_MS, ERROR_MAX, toSpec, LocalSandbox;
|
|
4498
|
+
var init_local = __esm({
|
|
4499
|
+
"src/sandbox/local.ts"() {
|
|
4500
|
+
"use strict";
|
|
4501
|
+
init_esm_shims();
|
|
4502
|
+
init_types2();
|
|
4503
|
+
execFileAsync = promisify(execFile);
|
|
4504
|
+
INSTALL_TIMEOUT_MS = 12e4;
|
|
4505
|
+
SMOKE_TIMEOUT_MS = 3e4;
|
|
4506
|
+
ERROR_MAX = 500;
|
|
4507
|
+
toSpec = (p) => p.version ? `${p.name}@${p.version}` : p.name;
|
|
4508
|
+
LocalSandbox = class {
|
|
4509
|
+
name = "local";
|
|
4510
|
+
async verify(pkg, version, opts = {}) {
|
|
4511
|
+
const set = await this.verifySet([{ name: pkg, version }], opts);
|
|
4512
|
+
return {
|
|
4513
|
+
driver: this.name,
|
|
4514
|
+
moduleSystem: set.moduleSystem,
|
|
4515
|
+
installed: set.installed,
|
|
4516
|
+
imported: set.loaded[0]?.loaded ?? null,
|
|
4517
|
+
ranScripts: opts.allowScripts ?? false,
|
|
4518
|
+
durationMs: set.durationMs,
|
|
4519
|
+
error: set.error
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
async verifySet(packages2, opts = {}) {
|
|
4523
|
+
const target = opts.target ?? DEFAULT_TARGET;
|
|
4524
|
+
const allowScripts = opts.allowScripts ?? false;
|
|
4525
|
+
const specs = packages2.map(toSpec);
|
|
4526
|
+
const dir = await mkdtemp(join3(tmpdir(), "lurq-sandbox-"));
|
|
4527
|
+
const started = Date.now();
|
|
4528
|
+
const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
|
|
4529
|
+
let installed = false;
|
|
4530
|
+
let error = null;
|
|
4531
|
+
try {
|
|
4532
|
+
await writeFile2(
|
|
4533
|
+
join3(dir, "package.json"),
|
|
4534
|
+
JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
|
|
4535
|
+
);
|
|
4536
|
+
await execFileAsync("npm", npmInstallArgs(specs, { allowScripts }), {
|
|
4537
|
+
cwd: dir,
|
|
4538
|
+
timeout: opts.timeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
4539
|
+
signal: opts.signal
|
|
4540
|
+
});
|
|
4541
|
+
installed = true;
|
|
4542
|
+
for (let i = 0; i < packages2.length; i++) {
|
|
4543
|
+
try {
|
|
4544
|
+
await execFileAsync("node", smokeScript(packages2[i].name, target.moduleSystem), {
|
|
4545
|
+
cwd: dir,
|
|
4546
|
+
timeout: SMOKE_TIMEOUT_MS,
|
|
4547
|
+
signal: opts.signal
|
|
4548
|
+
});
|
|
4549
|
+
loaded[i].loaded = true;
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
loaded[i].loaded = false;
|
|
4552
|
+
if (!error) error = condense(stderrOf(err));
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
} catch (err) {
|
|
4556
|
+
error = condense(stderrOf(err));
|
|
4557
|
+
} finally {
|
|
4558
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
return {
|
|
4562
|
+
driver: this.name,
|
|
4563
|
+
moduleSystem: target.moduleSystem,
|
|
4564
|
+
installed,
|
|
4565
|
+
loaded,
|
|
4566
|
+
durationMs: Date.now() - started,
|
|
4567
|
+
error
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
// src/sandbox/index.ts
|
|
4575
|
+
function getSandbox() {
|
|
4576
|
+
return new LocalSandbox();
|
|
4577
|
+
}
|
|
4578
|
+
var init_sandbox = __esm({
|
|
4579
|
+
"src/sandbox/index.ts"() {
|
|
4580
|
+
"use strict";
|
|
4581
|
+
init_esm_shims();
|
|
4582
|
+
init_local();
|
|
4583
|
+
init_types2();
|
|
4584
|
+
init_local();
|
|
4585
|
+
}
|
|
4586
|
+
});
|
|
4587
|
+
|
|
4588
|
+
// src/pipeline/sandbox.ts
|
|
4589
|
+
var sandbox_exports = {};
|
|
4590
|
+
__export(sandbox_exports, {
|
|
4591
|
+
verifyPackageInSandbox: () => verifyPackageInSandbox
|
|
4592
|
+
});
|
|
4593
|
+
async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
|
|
4594
|
+
const result = await getSandbox().verify(pkg, version, opts);
|
|
4595
|
+
await storeVerificationRun(db, {
|
|
4596
|
+
packageName: pkg,
|
|
4597
|
+
version: version ?? "latest",
|
|
4598
|
+
driver: result.driver,
|
|
4599
|
+
moduleSystem: result.moduleSystem,
|
|
4600
|
+
installed: result.installed,
|
|
4601
|
+
imported: result.imported,
|
|
4602
|
+
ranScripts: result.ranScripts,
|
|
4603
|
+
durationMs: result.durationMs,
|
|
4604
|
+
error: result.error,
|
|
4605
|
+
ranAt: /* @__PURE__ */ new Date()
|
|
4606
|
+
}).catch(() => {
|
|
4607
|
+
});
|
|
4608
|
+
return result;
|
|
4609
|
+
}
|
|
4610
|
+
var init_sandbox2 = __esm({
|
|
4611
|
+
"src/pipeline/sandbox.ts"() {
|
|
4612
|
+
"use strict";
|
|
4613
|
+
init_esm_shims();
|
|
4614
|
+
init_verification();
|
|
4615
|
+
init_sandbox();
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
|
|
4619
|
+
// src/pipeline/compat.ts
|
|
4620
|
+
var compat_exports = {};
|
|
4621
|
+
__export(compat_exports, {
|
|
4622
|
+
deriveCompatEdges: () => deriveCompatEdges,
|
|
4623
|
+
verifyCompatibility: () => verifyCompatibility
|
|
4624
|
+
});
|
|
4625
|
+
function deriveCompatEdges(resolved, result) {
|
|
4626
|
+
const allLoaded = result.loaded.every((l) => l.loaded === true);
|
|
4627
|
+
const edges = [];
|
|
4628
|
+
if (result.installed && allLoaded) {
|
|
4629
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
4630
|
+
for (let j = i + 1; j < resolved.length; j++) {
|
|
4631
|
+
edges.push({
|
|
4632
|
+
a: resolved[i].name,
|
|
4633
|
+
aVersion: resolved[i].version,
|
|
4634
|
+
b: resolved[j].name,
|
|
4635
|
+
bVersion: resolved[j].version,
|
|
4636
|
+
status: "compatible"
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
} else if (resolved.length === 2) {
|
|
4641
|
+
edges.push({
|
|
4642
|
+
a: resolved[0].name,
|
|
4643
|
+
aVersion: resolved[0].version,
|
|
4644
|
+
b: resolved[1].name,
|
|
4645
|
+
bVersion: resolved[1].version,
|
|
4646
|
+
status: "conflict"
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
4649
|
+
return edges;
|
|
4650
|
+
}
|
|
4651
|
+
async function verifyCompatibility(db, packages2, opts = {}) {
|
|
4652
|
+
const resolved = await Promise.all(
|
|
4653
|
+
packages2.map(async (name) => ({
|
|
4654
|
+
name,
|
|
4655
|
+
version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
|
|
4656
|
+
}))
|
|
4657
|
+
);
|
|
4658
|
+
const result = await getSandbox().verifySet(
|
|
4659
|
+
resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
|
|
4660
|
+
{ allowScripts: opts.allowScripts }
|
|
4661
|
+
);
|
|
4662
|
+
const edges = deriveCompatEdges(resolved, result);
|
|
4663
|
+
for (const e of edges) {
|
|
4664
|
+
const pair = canonicalPair(
|
|
4665
|
+
{ name: e.a, version: e.aVersion },
|
|
4666
|
+
{ name: e.b, version: e.bVersion }
|
|
4667
|
+
);
|
|
4668
|
+
await upsertCompatEdge(db, {
|
|
4669
|
+
...pair,
|
|
4670
|
+
status: e.status,
|
|
4671
|
+
driver: result.driver,
|
|
4672
|
+
ranAt: /* @__PURE__ */ new Date()
|
|
4673
|
+
}).catch(() => {
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
const failed = !result.installed || !result.loaded.every((l) => l.loaded === true);
|
|
4677
|
+
return { result, edges, unattributedConflict: failed && edges.length === 0 };
|
|
4678
|
+
}
|
|
4679
|
+
var init_compat2 = __esm({
|
|
4680
|
+
"src/pipeline/compat.ts"() {
|
|
4681
|
+
"use strict";
|
|
4682
|
+
init_esm_shims();
|
|
4683
|
+
init_compat();
|
|
4684
|
+
init_packages();
|
|
4685
|
+
init_sandbox();
|
|
4686
|
+
}
|
|
4687
|
+
});
|
|
4688
|
+
|
|
3180
4689
|
// src/cli/commands.ts
|
|
3181
4690
|
var commands_exports = {};
|
|
3182
4691
|
__export(commands_exports, {
|
|
3183
4692
|
runCompare: () => runCompare,
|
|
4693
|
+
runCompat: () => runCompat,
|
|
3184
4694
|
runEditWeights: () => runEditWeights,
|
|
3185
4695
|
runEvaluate: () => runEvaluate,
|
|
4696
|
+
runPlan: () => runPlan,
|
|
3186
4697
|
runRecommend: () => runRecommend,
|
|
4698
|
+
runSandbox: () => runSandbox,
|
|
3187
4699
|
runVerify: () => runVerify,
|
|
4700
|
+
runVersions: () => runVersions,
|
|
4701
|
+
runWatch: () => runWatch,
|
|
3188
4702
|
runWeights: () => runWeights
|
|
3189
4703
|
});
|
|
3190
4704
|
async function withDb(fn) {
|
|
@@ -3257,6 +4771,12 @@ async function runEvaluate(pkg, opts) {
|
|
|
3257
4771
|
["repo", res.repoUrl ?? "\u2014"]
|
|
3258
4772
|
])
|
|
3259
4773
|
);
|
|
4774
|
+
if (res.buildVerified) {
|
|
4775
|
+
const bv = res.buildVerified;
|
|
4776
|
+
const state = !bv.installed ? red("install failed") : bv.loaded === false ? yellow("installs, load failed") : green("installs and loads");
|
|
4777
|
+
console.log(`
|
|
4778
|
+
sandbox: ${state} ${dim(`${bv.version} \xB7 ${bv.driver}`)}`);
|
|
4779
|
+
}
|
|
3260
4780
|
if (res.summary) console.log("\n" + res.summary);
|
|
3261
4781
|
if (res.usageGuide) {
|
|
3262
4782
|
const g = res.usageGuide;
|
|
@@ -3332,11 +4852,13 @@ function runWeights(opts = {}) {
|
|
|
3332
4852
|
console.log(dim(`
|
|
3333
4853
|
Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
|
|
3334
4854
|
}
|
|
3335
|
-
function runEditWeights(opts) {
|
|
4855
|
+
async function runEditWeights(opts) {
|
|
4856
|
+
const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
3336
4857
|
if (opts.reset) {
|
|
3337
4858
|
const removed = resetWeights();
|
|
3338
4859
|
console.log(removed.length ? `Removed overrides:
|
|
3339
4860
|
${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
|
|
4861
|
+
if (removed.length) await invalidateCache2();
|
|
3340
4862
|
return;
|
|
3341
4863
|
}
|
|
3342
4864
|
if (opts.explain) {
|
|
@@ -3352,6 +4874,7 @@ function runEditWeights(opts) {
|
|
|
3352
4874
|
const next = applyOverrides(loadWeights(), opts.set);
|
|
3353
4875
|
const { weights, normalized } = validateWeights(next);
|
|
3354
4876
|
const path2 = saveWeights(weights, opts.project ? "project" : "user");
|
|
4877
|
+
await invalidateCache2();
|
|
3355
4878
|
console.log(`Saved overrides to ${path2}`);
|
|
3356
4879
|
if (normalized) {
|
|
3357
4880
|
console.log(
|
|
@@ -3365,11 +4888,83 @@ function runEditWeights(opts) {
|
|
|
3365
4888
|
`));
|
|
3366
4889
|
runWeights(opts);
|
|
3367
4890
|
}
|
|
4891
|
+
async function openInBrowser(target) {
|
|
4892
|
+
const { spawn } = await import("child_process");
|
|
4893
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", target]] : ["xdg-open", [target]];
|
|
4894
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
4895
|
+
}
|
|
4896
|
+
async function runPlan(file, opts) {
|
|
4897
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4898
|
+
let document;
|
|
4899
|
+
try {
|
|
4900
|
+
document = readFileSync4(file, "utf8");
|
|
4901
|
+
} catch {
|
|
4902
|
+
throw new Error(`Could not read "${file}".`);
|
|
4903
|
+
}
|
|
4904
|
+
if (opts.optimize && opts.optimize !== "speed" && opts.optimize !== "balanced") {
|
|
4905
|
+
throw new Error("--optimize must be 'speed' or 'balanced'.");
|
|
4906
|
+
}
|
|
4907
|
+
await withDb(async (db) => {
|
|
4908
|
+
const { handlePlan: handlePlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
4909
|
+
const res = await handlePlan2(db, {
|
|
4910
|
+
document,
|
|
4911
|
+
optimize: opts.optimize
|
|
4912
|
+
});
|
|
4913
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
4914
|
+
if (!("slots" in res)) {
|
|
4915
|
+
console.log(res.note);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
if (opts.html || opts.open) {
|
|
4919
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
4920
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
4921
|
+
const { join: join5 } = await import("path");
|
|
4922
|
+
const { renderPlanHtml: renderPlanHtml2 } = await Promise.resolve().then(() => (init_planView(), planView_exports));
|
|
4923
|
+
const out = opts.html ?? join5(tmpdir2(), `lurq-plan-${Date.now()}.html`);
|
|
4924
|
+
writeFileSync3(out, renderPlanHtml2(res), "utf8");
|
|
4925
|
+
console.log(`Roadmap written to ${out}`);
|
|
4926
|
+
if (opts.open) await openInBrowser(out);
|
|
4927
|
+
}
|
|
4928
|
+
console.log(
|
|
4929
|
+
table(
|
|
4930
|
+
["Component", "Layer", "Recommended", "Health", "Confidence", "Alternatives"],
|
|
4931
|
+
res.slots.map((s) => [
|
|
4932
|
+
s.need.length > 32 ? s.need.slice(0, 31) + "\u2026" : s.need,
|
|
4933
|
+
s.layer,
|
|
4934
|
+
s.recommended ? `${s.recommended.name}@${s.recommended.latestVersion ?? "?"}` : dim("\u2014"),
|
|
4935
|
+
s.recommended ? String(s.recommended.healthScore) : "\u2014",
|
|
4936
|
+
s.recommended ? confidenceLabel(s.recommended.confidence) : "\u2014",
|
|
4937
|
+
s.alternatives.map((a) => a.name).join(", ") || "\u2014"
|
|
4938
|
+
])
|
|
4939
|
+
)
|
|
4940
|
+
);
|
|
4941
|
+
if (res.unmatched.length) console.log(yellow(`
|
|
4942
|
+
No match for: ${res.unmatched.join(", ")}`));
|
|
4943
|
+
if (res.compatibility) {
|
|
4944
|
+
const c = res.compatibility;
|
|
4945
|
+
const col = c.overall === "compatible" ? green : c.overall === "conflict" ? red : dim;
|
|
4946
|
+
console.log("\n" + bold("Compatibility: ") + col(c.overall));
|
|
4947
|
+
for (const s of res.slots) {
|
|
4948
|
+
if (s.swappedFrom && s.recommended) {
|
|
4949
|
+
console.log(green(` \u2713 swapped ${s.swappedFrom} \u2192 ${s.recommended.name} for compatibility`));
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
for (const cf of c.conflicts) console.log(red(` \u2717 ${cf.detail} (no compatible alternative)`));
|
|
4953
|
+
if (c.unverified.length) console.log(dim(` unverified: ${c.unverified.join(", ")}`));
|
|
4954
|
+
}
|
|
4955
|
+
console.log("\n" + bold("Roadmap (Mermaid):"));
|
|
4956
|
+
console.log(res.mermaid);
|
|
4957
|
+
console.log(dim(`
|
|
4958
|
+
${res.note}`));
|
|
4959
|
+
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
3368
4962
|
async function runVerify(pkg, opts) {
|
|
3369
4963
|
await withDb(async (db) => {
|
|
3370
4964
|
const res = await handleVerify(db, { package: pkg });
|
|
3371
4965
|
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
3372
|
-
const
|
|
4966
|
+
const riskColor = res.risk === "high" ? red : res.risk === "medium" ? yellow : green;
|
|
4967
|
+
const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.risk === "high" ? red("\u2717 high supply-chain risk") : res.risk === "medium" ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
|
|
3373
4968
|
console.log(`${bold(pkg)} ${verdict}`);
|
|
3374
4969
|
console.log(
|
|
3375
4970
|
detail([
|
|
@@ -3377,11 +4972,107 @@ async function runVerify(pkg, opts) {
|
|
|
3377
4972
|
["weekly dl", formatNumber(res.weeklyDownloads)],
|
|
3378
4973
|
["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
|
|
3379
4974
|
["advisories", String(res.advisoryCount)],
|
|
4975
|
+
["risk", riskColor(res.risk)],
|
|
3380
4976
|
["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
|
|
3381
4977
|
])
|
|
3382
4978
|
);
|
|
3383
4979
|
});
|
|
3384
4980
|
}
|
|
4981
|
+
async function runVersions(pkg, opts) {
|
|
4982
|
+
const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 30) : 30;
|
|
4983
|
+
await withDb(async (db) => {
|
|
4984
|
+
const versions = await getPackageVersions(db, pkg, limit);
|
|
4985
|
+
if (opts.json) return console.log(JSON.stringify(versions, null, 2));
|
|
4986
|
+
if (versions.length === 0) {
|
|
4987
|
+
console.log(`No stored versions for ${bold(pkg)}. Run \`lurq sync --package ${pkg}\` first.`);
|
|
4988
|
+
return;
|
|
4989
|
+
}
|
|
4990
|
+
console.log(bold(pkg));
|
|
4991
|
+
console.log(
|
|
4992
|
+
table(
|
|
4993
|
+
["Version", "Published"],
|
|
4994
|
+
versions.map((v) => [
|
|
4995
|
+
v.version,
|
|
4996
|
+
v.publishedAt ? v.publishedAt.toISOString().slice(0, 10) : "\u2014"
|
|
4997
|
+
])
|
|
4998
|
+
)
|
|
4999
|
+
);
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
5002
|
+
async function runWatch() {
|
|
5003
|
+
const { watchNpmChanges: watchNpmChanges2 } = await Promise.resolve().then(() => (init_watch2(), watch_exports));
|
|
5004
|
+
await withDb(async (db) => {
|
|
5005
|
+
const controller = new AbortController();
|
|
5006
|
+
const stop = () => controller.abort();
|
|
5007
|
+
process.once("SIGINT", stop);
|
|
5008
|
+
process.once("SIGTERM", stop);
|
|
5009
|
+
console.log(dim("watching npm for releases of tracked packages \u2014 Ctrl-C to stop"));
|
|
5010
|
+
try {
|
|
5011
|
+
await watchNpmChanges2(db, { signal: controller.signal });
|
|
5012
|
+
} finally {
|
|
5013
|
+
process.off("SIGINT", stop);
|
|
5014
|
+
process.off("SIGTERM", stop);
|
|
5015
|
+
}
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
5018
|
+
async function runSandbox(pkg, version, opts) {
|
|
5019
|
+
if (opts.allowScripts) {
|
|
5020
|
+
console.error(
|
|
5021
|
+
yellow("warning: running install scripts and loading the package locally without isolation")
|
|
5022
|
+
);
|
|
5023
|
+
}
|
|
5024
|
+
const { verifyPackageInSandbox: verifyPackageInSandbox2 } = await Promise.resolve().then(() => (init_sandbox2(), sandbox_exports));
|
|
5025
|
+
await withDb(async (db) => {
|
|
5026
|
+
const result = await verifyPackageInSandbox2(db, pkg, version ?? null, {
|
|
5027
|
+
target: { node: "20", moduleSystem: opts.esm ? "esm" : "cjs" },
|
|
5028
|
+
allowScripts: opts.allowScripts
|
|
5029
|
+
});
|
|
5030
|
+
if (opts.json) return console.log(JSON.stringify(result, null, 2));
|
|
5031
|
+
const ok = result.installed && result.imported !== false;
|
|
5032
|
+
const label = version ? `${pkg}@${version}` : pkg;
|
|
5033
|
+
const verdict = ok ? green("\u2713 installs and loads") : red("\u2717 failed");
|
|
5034
|
+
console.log(`${bold(label)} ${verdict} ${dim(`(${result.durationMs}ms \xB7 ${result.driver})`)}`);
|
|
5035
|
+
console.log(
|
|
5036
|
+
detail([
|
|
5037
|
+
["installed", result.installed ? "yes" : "no"],
|
|
5038
|
+
["loaded", result.imported === null ? "\u2014" : result.imported ? "yes" : "no"],
|
|
5039
|
+
["module", result.moduleSystem],
|
|
5040
|
+
["scripts", result.ranScripts ? "ran" : "skipped"],
|
|
5041
|
+
["error", result.error ?? "none"]
|
|
5042
|
+
])
|
|
5043
|
+
);
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
async function runCompat(pkgs, opts) {
|
|
5047
|
+
await withDb(async (db) => {
|
|
5048
|
+
if (opts.run) {
|
|
5049
|
+
console.error(
|
|
5050
|
+
yellow("co-installing in the sandbox (loads package code locally without isolation)")
|
|
5051
|
+
);
|
|
5052
|
+
const { verifyCompatibility: verifyCompatibility2 } = await Promise.resolve().then(() => (init_compat2(), compat_exports));
|
|
5053
|
+
await verifyCompatibility2(db, pkgs);
|
|
5054
|
+
}
|
|
5055
|
+
const { handleCompat: handleCompat2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
|
|
5056
|
+
const res = await handleCompat2(db, { packages: pkgs });
|
|
5057
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
5058
|
+
const color = res.overall === "compatible" ? green : res.overall === "conflict" ? red : dim;
|
|
5059
|
+
console.log(`${bold(res.packages.join(" + "))} ${color(res.overall)}`);
|
|
5060
|
+
if (res.conflicts.length) {
|
|
5061
|
+
console.log(
|
|
5062
|
+
table(
|
|
5063
|
+
["Source", "Detail"],
|
|
5064
|
+
res.conflicts.map((c) => [c.source, c.detail])
|
|
5065
|
+
)
|
|
5066
|
+
);
|
|
5067
|
+
} else if (res.overall === "compatible") {
|
|
5068
|
+
console.log(dim("no peer-dependency or engine conflicts across the set"));
|
|
5069
|
+
}
|
|
5070
|
+
if (res.unverified.length) {
|
|
5071
|
+
console.log(dim(`
|
|
5072
|
+
unverified (no metadata): ${res.unverified.join(", ")}`));
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
3385
5076
|
var init_commands = __esm({
|
|
3386
5077
|
"src/cli/commands.ts"() {
|
|
3387
5078
|
"use strict";
|
|
@@ -3389,6 +5080,7 @@ var init_commands = __esm({
|
|
|
3389
5080
|
init_config();
|
|
3390
5081
|
init_types();
|
|
3391
5082
|
init_client();
|
|
5083
|
+
init_packages();
|
|
3392
5084
|
init_handlers();
|
|
3393
5085
|
init_weights();
|
|
3394
5086
|
init_weights();
|
|
@@ -3414,9 +5106,9 @@ __export(installSkill_exports, {
|
|
|
3414
5106
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3415
5107
|
import { copyFileSync } from "fs";
|
|
3416
5108
|
import { homedir as homedir3 } from "os";
|
|
3417
|
-
import { dirname as dirname4, join as
|
|
5109
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
3418
5110
|
function home(...p) {
|
|
3419
|
-
return
|
|
5111
|
+
return join4(homedir3(), ...p);
|
|
3420
5112
|
}
|
|
3421
5113
|
function agentSpecs() {
|
|
3422
5114
|
return [
|
|
@@ -3550,10 +5242,10 @@ function installAgent(spec, mode) {
|
|
|
3550
5242
|
}
|
|
3551
5243
|
}
|
|
3552
5244
|
function installInstructionsFile() {
|
|
3553
|
-
const src =
|
|
5245
|
+
const src = join4(packageRoot(), "templates", "skill-instructions.md");
|
|
3554
5246
|
if (!existsSync4(src)) return null;
|
|
3555
5247
|
const destDir = home(".lurq");
|
|
3556
|
-
const dest =
|
|
5248
|
+
const dest = join4(destDir, "skill-instructions.md");
|
|
3557
5249
|
mkdirSync2(destDir, { recursive: true });
|
|
3558
5250
|
copyFileSync(src, dest);
|
|
3559
5251
|
return dest;
|
|
@@ -3654,9 +5346,11 @@ async function validateKey(url, apiKey) {
|
|
|
3654
5346
|
},
|
|
3655
5347
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
3656
5348
|
});
|
|
3657
|
-
|
|
5349
|
+
if (res.ok) return "valid";
|
|
5350
|
+
if (res.status === 401 || res.status === 403) return "invalid";
|
|
5351
|
+
return "unreachable";
|
|
3658
5352
|
} catch {
|
|
3659
|
-
return
|
|
5353
|
+
return "unreachable";
|
|
3660
5354
|
}
|
|
3661
5355
|
}
|
|
3662
5356
|
async function runInstallWizard(opts) {
|
|
@@ -3664,7 +5358,7 @@ async function runInstallWizard(opts) {
|
|
|
3664
5358
|
const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
3665
5359
|
let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
|
|
3666
5360
|
if (interactive) {
|
|
3667
|
-
const { input, checkbox, confirm } = await import("@inquirer/prompts");
|
|
5361
|
+
const { input, checkbox, confirm, select } = await import("@inquirer/prompts");
|
|
3668
5362
|
console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
|
|
3669
5363
|
if (!apiKey) {
|
|
3670
5364
|
console.log(` Need a key? Get one at ${GET_KEY_URL}
|
|
@@ -3675,11 +5369,13 @@ async function runInstallWizard(opts) {
|
|
|
3675
5369
|
})).trim();
|
|
3676
5370
|
}
|
|
3677
5371
|
process.stdout.write(" Validating key\u2026 ");
|
|
3678
|
-
const
|
|
3679
|
-
console.log(
|
|
3680
|
-
|
|
5372
|
+
const check = await validateKey(url, apiKey);
|
|
5373
|
+
console.log(
|
|
5374
|
+
check === "valid" ? "ok" : check === "invalid" ? "rejected" : "could not reach endpoint"
|
|
5375
|
+
);
|
|
5376
|
+
if (check !== "valid") {
|
|
3681
5377
|
const proceed = await confirm({
|
|
3682
|
-
message:
|
|
5378
|
+
message: check === "invalid" ? `That key was rejected (401) by ${url}. Continue anyway?` : `Couldn't reach ${url} to validate the key. Continue anyway?`,
|
|
3683
5379
|
default: false
|
|
3684
5380
|
});
|
|
3685
5381
|
if (!proceed) {
|
|
@@ -3687,22 +5383,45 @@ async function runInstallWizard(opts) {
|
|
|
3687
5383
|
return;
|
|
3688
5384
|
}
|
|
3689
5385
|
}
|
|
3690
|
-
let selected2;
|
|
3691
5386
|
if (opts.agent) {
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
5387
|
+
await finish(resolveAgents(opts.agent), { url, apiKey });
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
const specs = agentSpecs();
|
|
5391
|
+
const detected = specs.filter((s) => s.detected);
|
|
5392
|
+
const primary = detected.find((s) => s.id === "claude-code") ?? detected[0];
|
|
5393
|
+
if (primary) {
|
|
5394
|
+
const others = detected.length - 1;
|
|
5395
|
+
const choice = await select({
|
|
5396
|
+
message: others > 0 ? `Looks like you have ${detected.map((s) => s.label).join(", ")}. Connect lurq to them?` : `Looks like you have ${primary.label}. Connect lurq to it?`,
|
|
5397
|
+
default: "yes",
|
|
5398
|
+
choices: [
|
|
5399
|
+
{
|
|
5400
|
+
name: others > 0 ? `Yes \u2014 set up all ${detected.length} detected agents` : `Yes \u2014 set up ${primary.label} for me`,
|
|
5401
|
+
value: "yes"
|
|
5402
|
+
},
|
|
5403
|
+
{ name: "No \u2014 let me choose which agent(s)", value: "other" },
|
|
5404
|
+
{ name: "Cancel \u2014 change nothing", value: "cancel" }
|
|
5405
|
+
]
|
|
3702
5406
|
});
|
|
3703
|
-
|
|
5407
|
+
if (choice === "cancel") {
|
|
5408
|
+
console.log("No problem \u2014 nothing was changed. Run `lurq install` again anytime.");
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
if (choice === "yes") {
|
|
5412
|
+
await finish(detected, { url, apiKey });
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
3704
5415
|
}
|
|
3705
|
-
await
|
|
5416
|
+
const ids = await checkbox({
|
|
5417
|
+
message: "Which assistant(s) should I configure?",
|
|
5418
|
+
choices: specs.map((s) => ({
|
|
5419
|
+
name: `${s.label}${s.detected ? " (detected)" : ""}`,
|
|
5420
|
+
value: s.id,
|
|
5421
|
+
checked: s.detected
|
|
5422
|
+
}))
|
|
5423
|
+
});
|
|
5424
|
+
await finish(specs.filter((s) => ids.includes(s.id)), { url, apiKey });
|
|
3706
5425
|
return;
|
|
3707
5426
|
}
|
|
3708
5427
|
if (!apiKey) {
|
|
@@ -3742,7 +5461,8 @@ var keys_exports = {};
|
|
|
3742
5461
|
__export(keys_exports, {
|
|
3743
5462
|
runKeysCreate: () => runKeysCreate,
|
|
3744
5463
|
runKeysList: () => runKeysList,
|
|
3745
|
-
runKeysRevoke: () => runKeysRevoke
|
|
5464
|
+
runKeysRevoke: () => runKeysRevoke,
|
|
5465
|
+
runKeysRotate: () => runKeysRotate
|
|
3746
5466
|
});
|
|
3747
5467
|
function waitForEnter(prompt) {
|
|
3748
5468
|
return new Promise((resolve) => {
|
|
@@ -3754,28 +5474,51 @@ function waitForEnter(prompt) {
|
|
|
3754
5474
|
});
|
|
3755
5475
|
});
|
|
3756
5476
|
}
|
|
5477
|
+
async function presentNewKey(key, row, opts) {
|
|
5478
|
+
if (opts.json) {
|
|
5479
|
+
console.log(
|
|
5480
|
+
JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label, ...opts.extraJson })
|
|
5481
|
+
);
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
|
|
5485
|
+
const block = [bold(opts.header ?? "API key created."), "", ` ${green(key)}`, "", dim(meta)];
|
|
5486
|
+
console.log(block.join("\n"));
|
|
5487
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
5488
|
+
await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
|
|
5489
|
+
process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
|
|
5490
|
+
console.log(
|
|
5491
|
+
dim(`New key (prefix ${row.prefix}) erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
|
|
5492
|
+
);
|
|
5493
|
+
} else {
|
|
5494
|
+
console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
3757
5497
|
async function runKeysCreate(opts) {
|
|
3758
5498
|
requireConfig(["DATABASE_URL"]);
|
|
3759
5499
|
const { db, close } = createDb({ max: 1 });
|
|
3760
5500
|
try {
|
|
3761
5501
|
const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
|
|
3762
|
-
|
|
3763
|
-
|
|
5502
|
+
await presentNewKey(key, row, opts);
|
|
5503
|
+
} finally {
|
|
5504
|
+
await close();
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
async function runKeysRotate(prefixOrId, opts) {
|
|
5508
|
+
requireConfig(["DATABASE_URL"]);
|
|
5509
|
+
const { db, close } = createDb({ max: 1 });
|
|
5510
|
+
try {
|
|
5511
|
+
const result = await rotateKey(db, prefixOrId);
|
|
5512
|
+
if (!result) {
|
|
5513
|
+
logger.warn(`No active key matched "${prefixOrId}".`);
|
|
5514
|
+
process.exitCode = 1;
|
|
3764
5515
|
return;
|
|
3765
5516
|
}
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
|
|
3772
|
-
process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
|
|
3773
|
-
console.log(
|
|
3774
|
-
dim(`API key created (prefix ${row.prefix}) \u2014 value erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
|
|
3775
|
-
);
|
|
3776
|
-
} else {
|
|
3777
|
-
console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
|
|
3778
|
-
}
|
|
5517
|
+
await presentNewKey(result.key, result.row, {
|
|
5518
|
+
json: opts.json,
|
|
5519
|
+
header: `API key rotated \u2014 replaces ${result.previous.prefix} (now revoked).`,
|
|
5520
|
+
extraJson: { replaced: result.previous.prefix }
|
|
5521
|
+
});
|
|
3779
5522
|
} finally {
|
|
3780
5523
|
await close();
|
|
3781
5524
|
}
|
|
@@ -3856,7 +5599,7 @@ var init_keys = __esm({
|
|
|
3856
5599
|
|
|
3857
5600
|
// src/db/seed.ts
|
|
3858
5601
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3859
|
-
import { sql as
|
|
5602
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
3860
5603
|
import { z as z3 } from "zod";
|
|
3861
5604
|
function loadSeedFile(path2 = seedJsonPath()) {
|
|
3862
5605
|
const raw = JSON.parse(readFileSync3(path2, "utf8"));
|
|
@@ -3873,7 +5616,7 @@ async function loadSeedPackages(db, path2) {
|
|
|
3873
5616
|
await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
|
|
3874
5617
|
target: seedPackages.name,
|
|
3875
5618
|
// Refresh category to the incoming value on conflict (EXCLUDED.category).
|
|
3876
|
-
set: { category:
|
|
5619
|
+
set: { category: sql5`excluded.category` }
|
|
3877
5620
|
});
|
|
3878
5621
|
logger.info(`Loaded ${entries.length} packages into seed_packages.`);
|
|
3879
5622
|
return entries.length;
|
|
@@ -3994,13 +5737,21 @@ function buildProgram() {
|
|
|
3994
5737
|
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
3995
5738
|
await runVerify2(pkg, opts);
|
|
3996
5739
|
});
|
|
5740
|
+
program.command("versions").argument("<package>", "npm package name").description("show the stored version timeline for a package").option("--json", "output JSON instead of a table").option("-n, --limit <n>", "how many versions to show (default 30)").action(async (pkg, opts) => {
|
|
5741
|
+
const { runVersions: runVersions2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5742
|
+
await runVersions2(pkg, opts);
|
|
5743
|
+
});
|
|
5744
|
+
program.command("plan").argument("<file>", "path to a markdown file describing your program").description("turn a program description into an evidence-scored package plan + roadmap").option("--optimize <mode>", "ranking bias: 'speed' (lightest bundle) or 'balanced'").option("--html <path>", "write the roadmap as a self-contained HTML visualization").option("--open", "render the roadmap to HTML and open it in your browser").option("--json", "output the full plan as JSON").action(async (file, opts) => {
|
|
5745
|
+
const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5746
|
+
await runPlan2(file, opts);
|
|
5747
|
+
});
|
|
3997
5748
|
program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
|
|
3998
5749
|
const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
3999
5750
|
runWeights2(opts);
|
|
4000
5751
|
});
|
|
4001
5752
|
program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
|
|
4002
5753
|
const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4003
|
-
runEditWeights2(opts);
|
|
5754
|
+
await runEditWeights2(opts);
|
|
4004
5755
|
});
|
|
4005
5756
|
program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
|
|
4006
5757
|
const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
@@ -4016,6 +5767,24 @@ function buildProgram() {
|
|
|
4016
5767
|
const summary = await runRescore2();
|
|
4017
5768
|
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
4018
5769
|
});
|
|
5770
|
+
program.command("watch").description(
|
|
5771
|
+
"operator-side: follow the npm changes feed, re-syncing tracked packages on new releases"
|
|
5772
|
+
).action(async () => {
|
|
5773
|
+
const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5774
|
+
await runWatch2();
|
|
5775
|
+
});
|
|
5776
|
+
program.command("sandbox").argument("<package>", "npm package name").argument("[version]", "specific version (default: latest)").description(
|
|
5777
|
+
"operator-side: install + smoke-load a package in a sandbox to verify it actually works"
|
|
5778
|
+
).option("--esm", "load via ESM import instead of CJS require").option("--allow-scripts", "run install scripts (UNSAFE without VM isolation)").option("--json", "output JSON").action(
|
|
5779
|
+
async (pkg, version, opts) => {
|
|
5780
|
+
const { runSandbox: runSandbox2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5781
|
+
await runSandbox2(pkg, version, opts);
|
|
5782
|
+
}
|
|
5783
|
+
);
|
|
5784
|
+
program.command("compat").argument("<packages...>", "npm package names to check together").description("check pairwise compatibility of packages from the sandbox matrix").option("--run", "operator-side: co-install them in the sandbox first (UNSAFE without VM isolation)").option("--json", "output JSON").action(async (pkgs, opts) => {
|
|
5785
|
+
const { runCompat: runCompat2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5786
|
+
await runCompat2(pkgs, opts);
|
|
5787
|
+
});
|
|
4019
5788
|
program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
|
|
4020
5789
|
const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4021
5790
|
await runInstallWizard2(opts);
|
|
@@ -4037,6 +5806,10 @@ function buildProgram() {
|
|
|
4037
5806
|
const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
4038
5807
|
await runKeysList2(opts);
|
|
4039
5808
|
});
|
|
5809
|
+
keys.command("rotate").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id to replace").description("issue a replacement key (same label/tier) and revoke the old one").option("--json", "print the new key as JSON and skip the interactive erase (for scripts)").action(async (prefixOrId, opts) => {
|
|
5810
|
+
const { runKeysRotate: runKeysRotate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5811
|
+
await runKeysRotate2(prefixOrId, opts);
|
|
5812
|
+
});
|
|
4040
5813
|
keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
|
|
4041
5814
|
const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
4042
5815
|
await runKeysRevoke2(prefixOrId);
|