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