lurqrun 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -201
- package/README.md +89 -61
- package/dist/bin/lurq.js +2648 -592
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +119 -3
- package/dist/index.js +2648 -589
- package/dist/index.js.map +1 -1
- package/drizzle/0028_romantic_loners.sql +2 -0
- package/drizzle/0029_busy_korvac.sql +16 -0
- package/drizzle/0030_steady_mathemanic.sql +6 -0
- package/drizzle/meta/0028_snapshot.json +2438 -0
- package/drizzle/meta/0029_snapshot.json +2546 -0
- package/drizzle/meta/0030_snapshot.json +2586 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +6 -3
- package/templates/agent-rules.md +5 -0
- package/templates/skill-instructions.md +14 -0
package/dist/bin/lurq.js
CHANGED
|
@@ -24,10 +24,10 @@ var init_package = __esm({
|
|
|
24
24
|
"package.json"() {
|
|
25
25
|
package_default = {
|
|
26
26
|
name: "lurqrun",
|
|
27
|
-
version: "0.1.
|
|
27
|
+
version: "0.1.1",
|
|
28
28
|
description: "A continuously-updated, evidence-scored index of JS/TS frameworks and libraries, exposed as an MCP server, CLI, and agent skill \u2014 so AI coding assistants get fresh, objective dependency recommendations.",
|
|
29
29
|
type: "module",
|
|
30
|
-
license: "
|
|
30
|
+
license: "MIT",
|
|
31
31
|
author: "jadenryu",
|
|
32
32
|
repository: {
|
|
33
33
|
type: "git",
|
|
@@ -81,7 +81,8 @@ var init_package = __esm({
|
|
|
81
81
|
operator: "tsx src/bin/operator.ts",
|
|
82
82
|
"operator:prod": "LURQ_ENV_FILE=.env.production tsx src/bin/operator.ts",
|
|
83
83
|
prepublishOnly: "npm test && npm run build",
|
|
84
|
-
"content:drift": "tsx scripts/gen-drift.ts"
|
|
84
|
+
"content:drift": "tsx scripts/gen-drift.ts",
|
|
85
|
+
"content:landing": "tsx scripts/build-landing-content.mts"
|
|
85
86
|
},
|
|
86
87
|
dependencies: {
|
|
87
88
|
"@inquirer/prompts": "^8.5.2",
|
|
@@ -96,8 +97,10 @@ var init_package = __esm({
|
|
|
96
97
|
postgres: "^3.4.9",
|
|
97
98
|
"rate-limit-redis": "^4.3.1",
|
|
98
99
|
semver: "^7.8.5",
|
|
100
|
+
stripe: "^22.6.0",
|
|
99
101
|
typescript: "^5.5.4",
|
|
100
102
|
"update-notifier": "^7.3.1",
|
|
103
|
+
yaml: "^2.9.0",
|
|
101
104
|
zod: "^3.23.8"
|
|
102
105
|
},
|
|
103
106
|
devDependencies: {
|
|
@@ -1013,10 +1016,10 @@ ${issues}`);
|
|
|
1013
1016
|
}
|
|
1014
1017
|
function requireConfig(keys) {
|
|
1015
1018
|
const config = getConfig();
|
|
1016
|
-
const
|
|
1017
|
-
if (
|
|
1019
|
+
const missing2 = keys.filter((k) => config[k] === void 0 || config[k] === "");
|
|
1020
|
+
if (missing2.length > 0) {
|
|
1018
1021
|
throw new ConfigError(
|
|
1019
|
-
`Missing required environment variable(s): ${
|
|
1022
|
+
`Missing required environment variable(s): ${missing2.join(", ")}.
|
|
1020
1023
|
See .env.example for setup. Tip: copy it to .env and fill in the values.`
|
|
1021
1024
|
);
|
|
1022
1025
|
}
|
|
@@ -1048,8 +1051,14 @@ var init_config = __esm({
|
|
|
1048
1051
|
* This is the rotation rate for everything discovery ingested: index size /
|
|
1049
1052
|
* this = days to come all the way around. Raise it if the sync finishes well
|
|
1050
1053
|
* inside the cron window; lower it if npm starts rate-limiting. 0 disables
|
|
1051
|
-
* the rotation (seeds only, the pre-rotation behaviour).
|
|
1052
|
-
|
|
1054
|
+
* the rotation (seeds only, the pre-rotation behaviour).
|
|
1055
|
+
*
|
|
1056
|
+
* It is also the discovery frontier's clock, which is why it is not a small
|
|
1057
|
+
* number. `graphChannel` re-expands a package only when its `latest_version`
|
|
1058
|
+
* moves, and nothing observes a version move except this rotation — so at 400
|
|
1059
|
+
* against a 20.8k index the graph channel could only re-arm on a 52-day lag
|
|
1060
|
+
* and had gone effectively silent. */
|
|
1061
|
+
LURQ_SYNC_REFRESH_CAP: z.coerce.number().int().min(0).max(25e3).default(2e3),
|
|
1053
1062
|
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
|
|
1054
1063
|
// Hosted HTTP service (`serve-http`). Server-side only.
|
|
1055
1064
|
PORT: z.coerce.number().int().positive().default(8080),
|
|
@@ -1065,6 +1074,22 @@ var init_config = __esm({
|
|
|
1065
1074
|
* authenticated web app presents it to mint a key for a signed-in user. Unset
|
|
1066
1075
|
* → the endpoint is disabled (404). Keep it server-side, never in the client. */
|
|
1067
1076
|
LURQ_ISSUER_SECRET: z.string().min(1).optional(),
|
|
1077
|
+
// Billing (Stripe). Every secret here lives on this service and nowhere else:
|
|
1078
|
+
// the web app holds no Stripe credential and reaches checkout through the
|
|
1079
|
+
// issuer-secret routes, the same way it reaches key issuance. Unset →
|
|
1080
|
+
// /billing/* is disabled (404) and every account is served the free tier, so
|
|
1081
|
+
// a deployment without Stripe configured degrades to "free for everyone"
|
|
1082
|
+
// rather than to "nobody can call anything".
|
|
1083
|
+
STRIPE_SECRET_KEY: z.string().min(1).optional(),
|
|
1084
|
+
/** Signing secret for `POST /billing/webhook`. Unset → the endpoint is 404. */
|
|
1085
|
+
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
|
|
1086
|
+
/** Price id backing the Pro plan. Unset → Pro cannot be checked out. */
|
|
1087
|
+
STRIPE_PRICE_PRO: z.string().min(1).optional(),
|
|
1088
|
+
/** Price id backing Enterprise. Normally unset: Enterprise is sold by
|
|
1089
|
+
* conversation, and `contactOnly` in core/plans.ts is what the page reads. */
|
|
1090
|
+
STRIPE_PRICE_ENTERPRISE: z.string().min(1).optional(),
|
|
1091
|
+
/** Absolute base URL the checkout returns to, e.g. https://lurq.run. */
|
|
1092
|
+
LURQ_WEB_URL: z.string().url().default("https://lurq.run"),
|
|
1068
1093
|
// GitHub App (repo autopilot). Only the App itself holds repo access; lurq
|
|
1069
1094
|
// never stores a user's personal token. Either unset → /repos is disabled (404).
|
|
1070
1095
|
// The App *slug* is deliberately not here: only the web app needs it, to build
|
|
@@ -1073,6 +1098,10 @@ var init_config = __esm({
|
|
|
1073
1098
|
LURQ_GITHUB_APP_ID: z.string().min(1).optional(),
|
|
1074
1099
|
/** PEM private key. Accepts literal newlines or `\n` escapes (Railway/Vercel). */
|
|
1075
1100
|
LURQ_GITHUB_APP_PRIVATE_KEY: z.string().min(1).optional(),
|
|
1101
|
+
/** Webhook secret from the same App settings page, so repo changes made on
|
|
1102
|
+
* GitHub reach us. Unset → `POST /github/webhook` is disabled (404): an
|
|
1103
|
+
* unverified webhook is an open write endpoint, so no secret means no route. */
|
|
1104
|
+
LURQ_GITHUB_WEBHOOK_SECRET: z.string().min(1).optional(),
|
|
1076
1105
|
// Client-side (install wizard / CLI talking to a remote endpoint).
|
|
1077
1106
|
LURQ_ENDPOINT: z.string().url().optional(),
|
|
1078
1107
|
LURQ_API_KEY: z.string().min(1).optional(),
|
|
@@ -1102,11 +1131,12 @@ function isCategory(value) {
|
|
|
1102
1131
|
function isFrontendCategory(category) {
|
|
1103
1132
|
return category != null && FRONTEND_CATEGORIES.includes(category);
|
|
1104
1133
|
}
|
|
1105
|
-
var CATEGORIES, FRONTEND_CATEGORIES;
|
|
1134
|
+
var DEFAULT_ECOSYSTEM, CATEGORIES, FRONTEND_CATEGORIES;
|
|
1106
1135
|
var init_types = __esm({
|
|
1107
1136
|
"src/core/types.ts"() {
|
|
1108
1137
|
"use strict";
|
|
1109
1138
|
init_esm_shims();
|
|
1139
|
+
DEFAULT_ECOSYSTEM = "npm";
|
|
1110
1140
|
CATEGORIES = [
|
|
1111
1141
|
"framework",
|
|
1112
1142
|
"meta-framework",
|
|
@@ -1147,11 +1177,255 @@ var init_types = __esm({
|
|
|
1147
1177
|
}
|
|
1148
1178
|
});
|
|
1149
1179
|
|
|
1180
|
+
// src/core/capabilities.ts
|
|
1181
|
+
function searchCapabilities(query, limit = 5) {
|
|
1182
|
+
const terms = tokenize(query ?? "");
|
|
1183
|
+
if (!terms.length) return CAPABILITIES.slice(0, limit).map((c) => ({ ...c, score: 0 }));
|
|
1184
|
+
const scored = CAPABILITIES.map((c) => {
|
|
1185
|
+
let score = 0;
|
|
1186
|
+
for (const { of, weight } of FIELDS) {
|
|
1187
|
+
const haystack = tokenize(of(c));
|
|
1188
|
+
for (const term of terms) {
|
|
1189
|
+
if (haystack.includes(term)) score += weight;
|
|
1190
|
+
else if (haystack.some((h) => sameStem(h, term))) score += weight / 2;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return { ...c, score };
|
|
1194
|
+
}).filter((c) => c.score > 0);
|
|
1195
|
+
if (!scored.length) return CAPABILITIES.slice(0, limit).map((c) => ({ ...c, score: 0 }));
|
|
1196
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1197
|
+
}
|
|
1198
|
+
var CAPABILITIES, STOP, tokenize, STEM_LEN, sameStem, FIELDS;
|
|
1199
|
+
var init_capabilities = __esm({
|
|
1200
|
+
"src/core/capabilities.ts"() {
|
|
1201
|
+
"use strict";
|
|
1202
|
+
init_esm_shims();
|
|
1203
|
+
CAPABILITIES = [
|
|
1204
|
+
{
|
|
1205
|
+
id: "verify",
|
|
1206
|
+
title: "Check a package is real",
|
|
1207
|
+
question: "Does this package actually exist, and is it safe to install?",
|
|
1208
|
+
answer: "Checks the live registry for existence, deprecation, archival and advisories \u2014 the guard against a hallucinated or typosquatted name reaching package.json.",
|
|
1209
|
+
mcp: "verify",
|
|
1210
|
+
cli: "lurq verify <package>",
|
|
1211
|
+
aliases: ["hallucinated", "typosquat", "slopsquat", "fake", "malicious", "safe", "exists"]
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
id: "recommend",
|
|
1215
|
+
title: "Pick a package for a need",
|
|
1216
|
+
question: "What should I use for this?",
|
|
1217
|
+
answer: "Returns evidence-scored candidates for a described need, with a confidence label on each. Ranks from current signals, not from what was popular at a training cutoff.",
|
|
1218
|
+
mcp: "recommend",
|
|
1219
|
+
cli: 'lurq recommend "<what you need>"',
|
|
1220
|
+
aliases: ["choose", "library", "best", "alternative", "which"]
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
id: "evaluate",
|
|
1224
|
+
title: "Read the evidence on a package",
|
|
1225
|
+
question: "Is this package healthy, maintained, and worth adopting?",
|
|
1226
|
+
answer: "The full evidence read for one package: health and quality scores with the signals behind them, release cadence, advisories, and how to wire it up.",
|
|
1227
|
+
mcp: "evaluate",
|
|
1228
|
+
cli: "lurq evaluate <package>",
|
|
1229
|
+
aliases: ["health", "maintained", "abandoned", "deprecated", "score", "quality"]
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "compare",
|
|
1233
|
+
title: "Compare packages side by side",
|
|
1234
|
+
question: "Which of these two should I go with?",
|
|
1235
|
+
answer: "Ranks 2\u20135 named packages against each other on the same scored evidence.",
|
|
1236
|
+
mcp: "compare",
|
|
1237
|
+
cli: "lurq compare <a> <b>",
|
|
1238
|
+
aliases: ["versus", "vs", "shootout", "better"]
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
id: "compat",
|
|
1242
|
+
title: "Check a stack fits together",
|
|
1243
|
+
question: "Will these packages work together?",
|
|
1244
|
+
answer: "Peer-dependency and engine checks across the whole set at once, plus any sandbox-verified conflicts on record. Returns the exact clashing constraints.",
|
|
1245
|
+
mcp: "compat",
|
|
1246
|
+
cli: "lurq compat <a> <b> <c>",
|
|
1247
|
+
aliases: ["peer", "conflict", "incompatible", "engines", "node version", "together"]
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
id: "usage",
|
|
1251
|
+
title: "See a version-exact API",
|
|
1252
|
+
question: "What does this package actually export at this version?",
|
|
1253
|
+
answer: "The exported symbols and signatures for one exact version, read from the shipped JS \u2014 plus the delta from a version you already know.",
|
|
1254
|
+
mcp: "usage",
|
|
1255
|
+
cli: "lurq usage <package> --target <version>",
|
|
1256
|
+
aliases: ["exports", "symbols", "api", "signature", "surface"]
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
id: "diff-surface",
|
|
1260
|
+
title: "See what an upgrade removes",
|
|
1261
|
+
question: "What breaks between these two versions?",
|
|
1262
|
+
answer: "The symbol-level diff between two published versions: what was removed, what changed arity, what is new. Runtime breakage is reported separately from type-only breakage.",
|
|
1263
|
+
mcp: "diff_surface",
|
|
1264
|
+
cli: "lurq usage <package> --known <old> --target <new>",
|
|
1265
|
+
aliases: ["breaking", "removed", "changed", "migration", "major"]
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
id: "plan",
|
|
1269
|
+
title: "Turn a spec into a stack",
|
|
1270
|
+
question: "I have a project description \u2014 what should I build it with?",
|
|
1271
|
+
answer: "Decomposes a written spec into component slots, recommends a scored package per slot, and optimises the set for compatibility. A starter stack, not an architecture.",
|
|
1272
|
+
mcp: "plan",
|
|
1273
|
+
cli: "lurq plan <file.md> --open",
|
|
1274
|
+
aliases: ["spec", "readme", "greenfield", "scaffold", "roadmap", "stack"]
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
id: "diagram",
|
|
1278
|
+
title: "Draw the stack",
|
|
1279
|
+
question: "Can I see this as a diagram?",
|
|
1280
|
+
answer: "Renders a planned or existing stack as a Mermaid graph other tools can parse.",
|
|
1281
|
+
mcp: "diagram",
|
|
1282
|
+
cli: "lurq plan <file.md> --html <out.html>",
|
|
1283
|
+
aliases: ["mermaid", "visual", "chart", "architecture", "graph"]
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
id: "upgrade-plan",
|
|
1287
|
+
title: "Find what is behind",
|
|
1288
|
+
question: "How far out of date is this project?",
|
|
1289
|
+
answer: "Reads the local manifests and reports the gap between what is resolved and what is current, with what each upgrade removes from its API. Sends dependency ranges only, never source.",
|
|
1290
|
+
cli: "lurq upgrade-plan",
|
|
1291
|
+
page: "/dashboard/repos",
|
|
1292
|
+
aliases: ["drift", "outdated", "behind", "stale", "update", "upgrade", "bump", "dependencies"]
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
id: "check-upgrade",
|
|
1296
|
+
title: "Check an upgrade against your code",
|
|
1297
|
+
question: "Will this upgrade break my code specifically?",
|
|
1298
|
+
answer: "Intersects what the upgrade removes with what your code actually references, and answers with file:line. Needs no test suite and no network to lurq.",
|
|
1299
|
+
cli: "lurq check-upgrade --plan plan.json --exit-code",
|
|
1300
|
+
aliases: ["breaking", "ci", "gate", "call sites", "references", "blocking"]
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
id: "check-release",
|
|
1304
|
+
title: "Check your own version bump",
|
|
1305
|
+
question: "Am I about to break my consumers?",
|
|
1306
|
+
answer: "Compares the built working tree against your last published version and says whether the bump you are about to tag covers what actually changed. For package authors.",
|
|
1307
|
+
cli: "lurq check-release --exit-code",
|
|
1308
|
+
aliases: ["publish", "semver", "release", "major", "prepublish", "library author"]
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "check-api",
|
|
1312
|
+
title: "Check your own API for breaks",
|
|
1313
|
+
question: "Will this change break the people calling my service?",
|
|
1314
|
+
answer: "Diffs two git revisions of your OpenAPI document: removed operations, newly required parameters and body fields, removed response codes \u2014 and whether info.version was bumped enough. Reads git only, nothing leaves the machine.",
|
|
1315
|
+
cli: "lurq check-api --against origin/main --exit-code",
|
|
1316
|
+
aliases: ["openapi", "swagger", "endpoint", "consumers", "callers", "service", "contract", "rest"]
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
id: "autopilot",
|
|
1320
|
+
title: "Keep a repo current automatically",
|
|
1321
|
+
question: "Can lurq just do the upgrades for me?",
|
|
1322
|
+
answer: "Connect a repo and lurq drafts the upgrade, names the call sites it breaks, and opens a PR from your own runner. lurq never gets write access to your code.",
|
|
1323
|
+
page: "/dashboard/repos",
|
|
1324
|
+
cli: "lurq upgrade-plan --json",
|
|
1325
|
+
aliases: ["automate", "pr", "github", "renovate", "dependabot", "autopilot", "workflow"]
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
id: "policy",
|
|
1329
|
+
title: "Set what agents may install",
|
|
1330
|
+
question: "How do I stop agents adding packages we do not allow?",
|
|
1331
|
+
answer: "An allow/deny list plus licence and confidence floors, enforced at the moment an agent reaches for a dependency \u2014 before it becomes a line in package.json.",
|
|
1332
|
+
page: "/dashboard/policy",
|
|
1333
|
+
aliases: ["allowlist", "whitelist", "deny", "blocklist", "licence", "license", "govern", "standard"]
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
id: "alerts",
|
|
1337
|
+
title: "Watch repos for new risk",
|
|
1338
|
+
question: "Tell me when something I depend on goes bad",
|
|
1339
|
+
answer: "Per-repo advisories and deprecations across the resolved tree, with the direct dependency responsible for each transitive finding.",
|
|
1340
|
+
page: "/dashboard/repos",
|
|
1341
|
+
aliases: ["advisory", "cve", "vulnerability", "security", "transitive", "sbom", "alert"]
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
id: "usage-dashboard",
|
|
1345
|
+
title: "See how lurq is being used",
|
|
1346
|
+
question: "Who on my team is calling lurq, and how much?",
|
|
1347
|
+
answer: "Call volume by tool and by day, plus the outcomes reported back after a recommendation.",
|
|
1348
|
+
page: "/dashboard/usage",
|
|
1349
|
+
aliases: ["activity", "analytics", "volume", "calls", "team", "adoption", "heatmap"]
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
id: "setup",
|
|
1353
|
+
title: "Connect lurq to an assistant",
|
|
1354
|
+
question: "How do I get this into Claude Code / Cursor?",
|
|
1355
|
+
answer: "One command detects every assistant on the machine and writes the MCP entry. No database credentials touch your machine.",
|
|
1356
|
+
cli: "npx lurqrun",
|
|
1357
|
+
page: "/dashboard/keys",
|
|
1358
|
+
aliases: ["install", "mcp", "cursor", "claude code", "windsurf", "copilot", "api key", "onboard"]
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
id: "weights",
|
|
1362
|
+
title: "Change how scoring works",
|
|
1363
|
+
question: "I disagree with the ranking \u2014 can I tune it?",
|
|
1364
|
+
answer: "Show the weight model, or layer your own overrides on top of the defaults, per user or per project.",
|
|
1365
|
+
cli: "lurq edit-weights --set composite.lambda=0.5",
|
|
1366
|
+
aliases: ["scoring", "ranking", "tune", "bias", "lambda", "override"]
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
id: "report-outcome",
|
|
1370
|
+
title: "Tell lurq how it went",
|
|
1371
|
+
question: "How does lurq learn from what I picked?",
|
|
1372
|
+
answer: "An opt-in report of whether you took the recommendation and whether it built. Coarse signal only \u2014 never source code.",
|
|
1373
|
+
mcp: "report_outcome",
|
|
1374
|
+
aliases: ["feedback", "outcome", "accepted", "flywheel", "learn"]
|
|
1375
|
+
}
|
|
1376
|
+
];
|
|
1377
|
+
STOP = /* @__PURE__ */ new Set([
|
|
1378
|
+
"a",
|
|
1379
|
+
"an",
|
|
1380
|
+
"the",
|
|
1381
|
+
"is",
|
|
1382
|
+
"it",
|
|
1383
|
+
"i",
|
|
1384
|
+
"my",
|
|
1385
|
+
"me",
|
|
1386
|
+
"do",
|
|
1387
|
+
"does",
|
|
1388
|
+
"how",
|
|
1389
|
+
"what",
|
|
1390
|
+
"which",
|
|
1391
|
+
"to",
|
|
1392
|
+
"for",
|
|
1393
|
+
"of",
|
|
1394
|
+
"in",
|
|
1395
|
+
"on",
|
|
1396
|
+
"and",
|
|
1397
|
+
"or",
|
|
1398
|
+
"can",
|
|
1399
|
+
"with",
|
|
1400
|
+
"this",
|
|
1401
|
+
"that",
|
|
1402
|
+
"be",
|
|
1403
|
+
"am",
|
|
1404
|
+
"are",
|
|
1405
|
+
"should",
|
|
1406
|
+
"lurq",
|
|
1407
|
+
"package",
|
|
1408
|
+
"packages"
|
|
1409
|
+
]);
|
|
1410
|
+
tokenize = (s) => s.toLowerCase().split(/[^a-z0-9.@/]+/).filter((t) => t.length > 1 && !STOP.has(t));
|
|
1411
|
+
STEM_LEN = 5;
|
|
1412
|
+
sameStem = (a, b) => a.length >= STEM_LEN && b.length >= STEM_LEN && a.slice(0, STEM_LEN) === b.slice(0, STEM_LEN);
|
|
1413
|
+
FIELDS = [
|
|
1414
|
+
{ of: (c) => c.title, weight: 5 },
|
|
1415
|
+
{ of: (c) => c.question, weight: 4 },
|
|
1416
|
+
{ of: (c) => (c.aliases ?? []).join(" "), weight: 3 },
|
|
1417
|
+
{ of: (c) => c.answer, weight: 1 },
|
|
1418
|
+
{ of: (c) => [c.mcp, c.cli, c.page].filter(Boolean).join(" "), weight: 4 }
|
|
1419
|
+
];
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1150
1423
|
// src/db/schema.ts
|
|
1151
1424
|
var schema_exports = {};
|
|
1152
1425
|
__export(schema_exports, {
|
|
1153
1426
|
apiKeys: () => apiKeys,
|
|
1154
1427
|
apiSurfaces: () => apiSurfaces,
|
|
1428
|
+
askSpendDaily: () => askSpendDaily,
|
|
1155
1429
|
claims: () => claims,
|
|
1156
1430
|
compatEdges: () => compatEdges,
|
|
1157
1431
|
compatVerifyQueue: () => compatVerifyQueue,
|
|
@@ -1168,6 +1442,7 @@ __export(schema_exports, {
|
|
|
1168
1442
|
resolvedClosures: () => resolvedClosures,
|
|
1169
1443
|
seedPackages: () => seedPackages,
|
|
1170
1444
|
selectionPolicies: () => selectionPolicies,
|
|
1445
|
+
subscriptions: () => subscriptions,
|
|
1171
1446
|
surfaceQueue: () => surfaceQueue,
|
|
1172
1447
|
symbols: () => symbols,
|
|
1173
1448
|
syncRuns: () => syncRuns,
|
|
@@ -1193,7 +1468,7 @@ import {
|
|
|
1193
1468
|
uniqueIndex,
|
|
1194
1469
|
vector
|
|
1195
1470
|
} from "drizzle-orm/pg-core";
|
|
1196
|
-
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, compatVerifyQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges, resolvedClosures, apiSurfaces, recommendationOutcomes, ownerUsageDaily, repos, upgradeRuns, repoAlerts, surfaceQueue, entities, symbols, environments, claims, observations, selectionPolicies;
|
|
1471
|
+
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, compatVerifyQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges, resolvedClosures, apiSurfaces, recommendationOutcomes, askSpendDaily, ownerUsageDaily, repos, upgradeRuns, repoAlerts, surfaceQueue, entities, symbols, environments, claims, observations, selectionPolicies, subscriptions;
|
|
1197
1472
|
var init_schema = __esm({
|
|
1198
1473
|
"src/db/schema.ts"() {
|
|
1199
1474
|
"use strict";
|
|
@@ -1209,8 +1484,8 @@ var init_schema = __esm({
|
|
|
1209
1484
|
"packages",
|
|
1210
1485
|
{
|
|
1211
1486
|
id: serial("id").primaryKey(),
|
|
1212
|
-
name: text("name").notNull()
|
|
1213
|
-
ecosystem: text("ecosystem").notNull().default("npm"),
|
|
1487
|
+
name: text("name").notNull(),
|
|
1488
|
+
ecosystem: text("ecosystem").$type().notNull().default("npm"),
|
|
1214
1489
|
// Classification + descriptive text
|
|
1215
1490
|
category: text("category").$type(),
|
|
1216
1491
|
/** Whether `category` was hand-curated or inferred at ingest (§2A). */
|
|
@@ -1282,6 +1557,7 @@ var init_schema = __esm({
|
|
|
1282
1557
|
firstRequestedByOwnerId: text("first_requested_by_owner_id")
|
|
1283
1558
|
},
|
|
1284
1559
|
(table2) => [
|
|
1560
|
+
uniqueIndex("packages_ecosystem_name_idx").on(table2.ecosystem, table2.name),
|
|
1285
1561
|
index("packages_category_idx").on(table2.category),
|
|
1286
1562
|
index("packages_health_score_idx").on(table2.healthScore),
|
|
1287
1563
|
// pgvector HNSW index for cosine similarity search (§11).
|
|
@@ -1435,9 +1711,7 @@ var init_schema = __esm({
|
|
|
1435
1711
|
nodes: jsonb("nodes").$type().notNull(),
|
|
1436
1712
|
fetchedAt: ts("fetched_at").notNull().defaultNow()
|
|
1437
1713
|
},
|
|
1438
|
-
(table2) => [
|
|
1439
|
-
uniqueIndex("resolved_closures_pkg_idx").on(table2.packageName, table2.version)
|
|
1440
|
-
]
|
|
1714
|
+
(table2) => [uniqueIndex("resolved_closures_pkg_idx").on(table2.packageName, table2.version)]
|
|
1441
1715
|
);
|
|
1442
1716
|
apiSurfaces = pgTable(
|
|
1443
1717
|
"api_surfaces",
|
|
@@ -1473,6 +1747,16 @@ var init_schema = __esm({
|
|
|
1473
1747
|
index("recommendation_outcomes_owner_idx").on(table2.ownerId)
|
|
1474
1748
|
]
|
|
1475
1749
|
);
|
|
1750
|
+
askSpendDaily = pgTable(
|
|
1751
|
+
"ask_spend_daily",
|
|
1752
|
+
{
|
|
1753
|
+
ownerId: text("owner_id").notNull(),
|
|
1754
|
+
/** UTC day, 'YYYY-MM-DD'. */
|
|
1755
|
+
date: date("date").notNull(),
|
|
1756
|
+
usdMicros: bigint("usd_micros", { mode: "number" }).notNull().default(0)
|
|
1757
|
+
},
|
|
1758
|
+
(table2) => [primaryKey({ columns: [table2.ownerId, table2.date] })]
|
|
1759
|
+
);
|
|
1476
1760
|
ownerUsageDaily = pgTable(
|
|
1477
1761
|
"owner_usage_daily",
|
|
1478
1762
|
{
|
|
@@ -1601,11 +1885,7 @@ var init_schema = __esm({
|
|
|
1601
1885
|
// One alert per repo per release. A re-sync of the same version — and the
|
|
1602
1886
|
// watcher re-syncs on every publish, including non-latest backports — must
|
|
1603
1887
|
// not re-notify.
|
|
1604
|
-
uniqueIndex("repo_alerts_dedup_idx").on(
|
|
1605
|
-
table2.repoId,
|
|
1606
|
-
table2.packageName,
|
|
1607
|
-
table2.toVersion
|
|
1608
|
-
)
|
|
1888
|
+
uniqueIndex("repo_alerts_dedup_idx").on(table2.repoId, table2.packageName, table2.toVersion)
|
|
1609
1889
|
]
|
|
1610
1890
|
);
|
|
1611
1891
|
surfaceQueue = pgTable(
|
|
@@ -1681,7 +1961,9 @@ var init_schema = __esm({
|
|
|
1681
1961
|
// they are not interchangeable (§6.4.3). Without the tier in the key, storing a
|
|
1682
1962
|
// tier-C surface would silently overwrite the tier-A one that answers runtime
|
|
1683
1963
|
// existence — replacing the authoritative answer with a type-level guess.
|
|
1684
|
-
(table2) => [
|
|
1964
|
+
(table2) => [
|
|
1965
|
+
uniqueIndex("symbols_entity_path_tier_idx").on(table2.entityId, table2.path, table2.tier)
|
|
1966
|
+
]
|
|
1685
1967
|
);
|
|
1686
1968
|
environments = pgTable("environments", {
|
|
1687
1969
|
id: serial("id").primaryKey(),
|
|
@@ -1742,6 +2024,33 @@ var init_schema = __esm({
|
|
|
1742
2024
|
policy: jsonb("policy").$type().notNull(),
|
|
1743
2025
|
updatedAt: ts("updated_at").notNull().defaultNow()
|
|
1744
2026
|
});
|
|
2027
|
+
subscriptions = pgTable(
|
|
2028
|
+
"subscriptions",
|
|
2029
|
+
{
|
|
2030
|
+
/** Clerk user id. One subscription per account. */
|
|
2031
|
+
ownerId: text("owner_id").primaryKey(),
|
|
2032
|
+
/** Stable across subscriptions: a customer who cancels and returns reuses it. */
|
|
2033
|
+
stripeCustomerId: text("stripe_customer_id").notNull().unique(),
|
|
2034
|
+
/** Null between creating a customer and completing a first checkout. */
|
|
2035
|
+
stripeSubscriptionId: text("stripe_subscription_id").unique(),
|
|
2036
|
+
tier: text("tier").$type().notNull().default("free"),
|
|
2037
|
+
/** Raw Stripe subscription status. `null` = never subscribed. */
|
|
2038
|
+
status: text("status"),
|
|
2039
|
+
/** When the paid period lapses. Display only; `status` is what gates access. */
|
|
2040
|
+
currentPeriodEnd: ts("current_period_end"),
|
|
2041
|
+
/** Set when the user cancels but has paid through the period. */
|
|
2042
|
+
cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull().default(false),
|
|
2043
|
+
/**
|
|
2044
|
+
* Stripe delivers out of order and retries, so a late duplicate of an older
|
|
2045
|
+
* event must not overwrite newer state. The webhook drops any event whose
|
|
2046
|
+
* timestamp is older than the one that produced the current row.
|
|
2047
|
+
*/
|
|
2048
|
+
lastEventAt: ts("last_event_at"),
|
|
2049
|
+
createdAt: ts("created_at").notNull().defaultNow(),
|
|
2050
|
+
updatedAt: ts("updated_at").notNull().defaultNow()
|
|
2051
|
+
},
|
|
2052
|
+
(table2) => [index("subscriptions_customer_idx").on(table2.stripeCustomerId)]
|
|
2053
|
+
);
|
|
1745
2054
|
}
|
|
1746
2055
|
});
|
|
1747
2056
|
|
|
@@ -1750,28 +2059,34 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
1750
2059
|
import postgres from "postgres";
|
|
1751
2060
|
function createDb(opts = {}) {
|
|
1752
2061
|
const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2062
|
+
const sql16 = postgres(DATABASE_URL, {
|
|
2063
|
+
max: opts.max ?? 10,
|
|
2064
|
+
onnotice: () => {
|
|
2065
|
+
},
|
|
2066
|
+
connection: { statement_timeout: STATEMENT_TIMEOUT_MS }
|
|
2067
|
+
});
|
|
2068
|
+
const db = drizzle(sql16, { schema: schema_exports });
|
|
2069
|
+
return { db, sql: sql16, close: () => sql16.end() };
|
|
1757
2070
|
}
|
|
2071
|
+
var STATEMENT_TIMEOUT_MS;
|
|
1758
2072
|
var init_client = __esm({
|
|
1759
2073
|
"src/db/client.ts"() {
|
|
1760
2074
|
"use strict";
|
|
1761
2075
|
init_esm_shims();
|
|
1762
2076
|
init_config();
|
|
1763
2077
|
init_schema();
|
|
2078
|
+
STATEMENT_TIMEOUT_MS = 15e3;
|
|
1764
2079
|
}
|
|
1765
2080
|
});
|
|
1766
2081
|
|
|
1767
2082
|
// src/db/packages.ts
|
|
1768
|
-
import { and, desc, eq, inArray, isNotNull, isNull, lt, notExists, sql as sql2 } from "drizzle-orm";
|
|
1769
|
-
async function getPackageByName(db, name) {
|
|
1770
|
-
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
2083
|
+
import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, notExists, sql as sql2 } from "drizzle-orm";
|
|
2084
|
+
async function getPackageByName(db, name, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
2085
|
+
const rows = await db.select().from(packages).where(and(eq(packages.name, name), eq(packages.ecosystem, ecosystem))).limit(1);
|
|
1771
2086
|
return rows[0] ?? null;
|
|
1772
2087
|
}
|
|
1773
|
-
async function getAllPackageNames(db) {
|
|
1774
|
-
const rows = await db.select({ name: packages.name }).from(packages);
|
|
2088
|
+
async function getAllPackageNames(db, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
2089
|
+
const rows = await db.select({ name: packages.name }).from(packages).where(eq(packages.ecosystem, ecosystem));
|
|
1775
2090
|
return rows.map((r) => r.name);
|
|
1776
2091
|
}
|
|
1777
2092
|
async function upsertPackageVersions(db, name, versions) {
|
|
@@ -1802,14 +2117,20 @@ async function ensureSeedEntry(db, name, category) {
|
|
|
1802
2117
|
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1803
2118
|
}
|
|
1804
2119
|
async function upsertPackage(db, row) {
|
|
1805
|
-
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
2120
|
+
const { name: _name, ecosystem: _ecosystem, createdAt: _createdAt, ...mutable } = row;
|
|
1806
2121
|
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1807
|
-
target: packages.name,
|
|
2122
|
+
target: [packages.ecosystem, packages.name],
|
|
1808
2123
|
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1809
2124
|
});
|
|
1810
2125
|
}
|
|
1811
|
-
async function stampFirstRequester(db, name, ownerId) {
|
|
1812
|
-
await db.update(packages).set({ firstRequestedByOwnerId: ownerId }).where(
|
|
2126
|
+
async function stampFirstRequester(db, name, ownerId, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
2127
|
+
await db.update(packages).set({ firstRequestedByOwnerId: ownerId }).where(
|
|
2128
|
+
and(
|
|
2129
|
+
eq(packages.name, name),
|
|
2130
|
+
eq(packages.ecosystem, ecosystem),
|
|
2131
|
+
isNull(packages.firstRequestedByOwnerId)
|
|
2132
|
+
)
|
|
2133
|
+
);
|
|
1813
2134
|
}
|
|
1814
2135
|
async function getContributionsByOwner(db, ownerId, opts = {}) {
|
|
1815
2136
|
const limit = opts.limit ?? 50;
|
|
@@ -1831,6 +2152,7 @@ var init_packages = __esm({
|
|
|
1831
2152
|
"use strict";
|
|
1832
2153
|
init_esm_shims();
|
|
1833
2154
|
init_logger();
|
|
2155
|
+
init_types();
|
|
1834
2156
|
init_schema();
|
|
1835
2157
|
VERSION_CHUNK = 500;
|
|
1836
2158
|
SYNC_STALE_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -1879,13 +2201,16 @@ async function namespaceVersion(client) {
|
|
|
1879
2201
|
versionMemo = { value, at: Date.now() };
|
|
1880
2202
|
return value;
|
|
1881
2203
|
}
|
|
2204
|
+
function codeVersion() {
|
|
2205
|
+
return VERSION;
|
|
2206
|
+
}
|
|
1882
2207
|
async function cached2(namespace, key, compute, opts = {}) {
|
|
1883
2208
|
const client = await getClient();
|
|
1884
2209
|
if (!client) return compute();
|
|
1885
2210
|
let fullKey;
|
|
1886
2211
|
try {
|
|
1887
2212
|
const version = await namespaceVersion(client);
|
|
1888
|
-
fullKey = `lurq:${namespace}:${version}:${key}`;
|
|
2213
|
+
fullKey = `lurq:${namespace}:${codeVersion()}:${version}:${key}`;
|
|
1889
2214
|
const hit = await client.get(fullKey);
|
|
1890
2215
|
if (hit != null) return JSON.parse(hit);
|
|
1891
2216
|
} catch (err) {
|
|
@@ -1914,6 +2239,7 @@ var init_cache = __esm({
|
|
|
1914
2239
|
"src/core/cache.ts"() {
|
|
1915
2240
|
"use strict";
|
|
1916
2241
|
init_esm_shims();
|
|
2242
|
+
init_constants();
|
|
1917
2243
|
init_logger();
|
|
1918
2244
|
VERSION_KEY = "lurq:cachever";
|
|
1919
2245
|
DEFAULT_TTL_SEC = 3600;
|
|
@@ -1925,7 +2251,7 @@ var init_cache = __esm({
|
|
|
1925
2251
|
|
|
1926
2252
|
// src/db/compat.ts
|
|
1927
2253
|
import { and as and2, eq as eq2, inArray as inArray2, sql as sql3 } from "drizzle-orm";
|
|
1928
|
-
async function getCompatMetadata(db, names2) {
|
|
2254
|
+
async function getCompatMetadata(db, names2, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
1929
2255
|
if (names2.length === 0) return [];
|
|
1930
2256
|
return db.select({
|
|
1931
2257
|
name: packages.name,
|
|
@@ -1933,7 +2259,7 @@ async function getCompatMetadata(db, names2) {
|
|
|
1933
2259
|
peerDependencies: packages.peerDependencies,
|
|
1934
2260
|
peerDependenciesMeta: packages.peerDependenciesMeta,
|
|
1935
2261
|
engines: packages.engines
|
|
1936
|
-
}).from(packages).where(inArray2(packages.name, names2));
|
|
2262
|
+
}).from(packages).where(and2(inArray2(packages.name, names2), eq2(packages.ecosystem, ecosystem)));
|
|
1937
2263
|
}
|
|
1938
2264
|
function canonicalPair(a, b) {
|
|
1939
2265
|
const [low, high] = a.name <= b.name ? [a, b] : [b, a];
|
|
@@ -2027,6 +2353,7 @@ var init_compat = __esm({
|
|
|
2027
2353
|
"src/db/compat.ts"() {
|
|
2028
2354
|
"use strict";
|
|
2029
2355
|
init_esm_shims();
|
|
2356
|
+
init_types();
|
|
2030
2357
|
init_schema();
|
|
2031
2358
|
CONFLICT_TARGET = [
|
|
2032
2359
|
compatEdges.packageA,
|
|
@@ -2171,7 +2498,7 @@ function parseRetryAfter(value) {
|
|
|
2171
2498
|
return Number.isNaN(date2) ? void 0 : Math.max(0, date2 - Date.now());
|
|
2172
2499
|
}
|
|
2173
2500
|
function delay(ms) {
|
|
2174
|
-
return new Promise((
|
|
2501
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2175
2502
|
}
|
|
2176
2503
|
var HttpError, DEFAULT_HOST_CONFIG, HOST_CONFIG, HostLimiter, limiters, MEMORY_CACHE_MAX, memoryCache, bypassCacheRead, RETRYABLE;
|
|
2177
2504
|
var init_http = __esm({
|
|
@@ -2211,7 +2538,7 @@ var init_http = __esm({
|
|
|
2211
2538
|
queue = [];
|
|
2212
2539
|
async acquire() {
|
|
2213
2540
|
if (this.active >= this.config.maxConcurrent) {
|
|
2214
|
-
await new Promise((
|
|
2541
|
+
await new Promise((resolve2) => this.queue.push(resolve2));
|
|
2215
2542
|
}
|
|
2216
2543
|
this.active++;
|
|
2217
2544
|
const now = Date.now();
|
|
@@ -2401,6 +2728,20 @@ async function fetchNpmCompatAtVersion(name, version, fetchImpl) {
|
|
|
2401
2728
|
return null;
|
|
2402
2729
|
}
|
|
2403
2730
|
}
|
|
2731
|
+
async function npmVersionExists(name, version, fetchImpl) {
|
|
2732
|
+
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
2733
|
+
try {
|
|
2734
|
+
const { data } = await httpGetJson(url, {
|
|
2735
|
+
host: HOST,
|
|
2736
|
+
ttlMs: CACHE_TTL.npmRegistry,
|
|
2737
|
+
fetchImpl
|
|
2738
|
+
});
|
|
2739
|
+
if (!data?.versions || typeof data.versions !== "object") return null;
|
|
2740
|
+
return Object.prototype.hasOwnProperty.call(data.versions, version);
|
|
2741
|
+
} catch {
|
|
2742
|
+
return null;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2404
2745
|
async function npmPackageExists(name, fetchImpl) {
|
|
2405
2746
|
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
2406
2747
|
try {
|
|
@@ -3048,6 +3389,23 @@ var init_diff = __esm({
|
|
|
3048
3389
|
}
|
|
3049
3390
|
});
|
|
3050
3391
|
|
|
3392
|
+
// src/core/concurrency.ts
|
|
3393
|
+
function withBudget(task, ms) {
|
|
3394
|
+
let timer;
|
|
3395
|
+
return Promise.race([
|
|
3396
|
+
task,
|
|
3397
|
+
new Promise((resolve2) => {
|
|
3398
|
+
timer = setTimeout(resolve2, ms, null);
|
|
3399
|
+
})
|
|
3400
|
+
]).finally(() => clearTimeout(timer));
|
|
3401
|
+
}
|
|
3402
|
+
var init_concurrency = __esm({
|
|
3403
|
+
"src/core/concurrency.ts"() {
|
|
3404
|
+
"use strict";
|
|
3405
|
+
init_esm_shims();
|
|
3406
|
+
}
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3051
3409
|
// src/db/apiSurfaces.ts
|
|
3052
3410
|
import { and as and3, eq as eq3, isNotNull as isNotNull2, isNull as isNull2, sql as sql4 } from "drizzle-orm";
|
|
3053
3411
|
async function getStoredSurface(db, packageName, version) {
|
|
@@ -3092,8 +3450,8 @@ async function fetchFile(url, opts, ttlMs = CACHE_TTL.depsDev) {
|
|
|
3092
3450
|
});
|
|
3093
3451
|
return { text: typeof data === "string" ? data : null, unknown: false };
|
|
3094
3452
|
} catch (err) {
|
|
3095
|
-
const
|
|
3096
|
-
return { text: null, unknown: !
|
|
3453
|
+
const missing2 = err instanceof HttpError && err.status === 404;
|
|
3454
|
+
return { text: null, unknown: !missing2 };
|
|
3097
3455
|
}
|
|
3098
3456
|
}
|
|
3099
3457
|
async function fetchText(url, opts) {
|
|
@@ -3341,9 +3699,6 @@ var init_extract = __esm({
|
|
|
3341
3699
|
});
|
|
3342
3700
|
|
|
3343
3701
|
// src/usage/service.ts
|
|
3344
|
-
function raceBudget(task, budgetMs) {
|
|
3345
|
-
return Promise.race([task, new Promise((resolve) => setTimeout(resolve, budgetMs, null))]);
|
|
3346
|
-
}
|
|
3347
3702
|
function extractOnce(db, name, version, fetchOpts) {
|
|
3348
3703
|
const key = `${name}@${version}`;
|
|
3349
3704
|
const existing = inFlight.get(key);
|
|
@@ -3363,13 +3718,14 @@ async function getOrExtractSurface(db, name, version, opts = {}) {
|
|
|
3363
3718
|
if (opts.budgetMs === 0) return null;
|
|
3364
3719
|
const fetchOpts = opts.fetch ?? (opts.budgetMs === void 0 ? {} : READ_PATH_FETCH);
|
|
3365
3720
|
const task = extractOnce(db, name, version, fetchOpts);
|
|
3366
|
-
return opts.budgetMs === void 0 ? task :
|
|
3721
|
+
return opts.budgetMs === void 0 ? task : withBudget(task, opts.budgetMs);
|
|
3367
3722
|
}
|
|
3368
3723
|
var USAGE_EXTRACT_BUDGET_MS, READ_PATH_FETCH, MAX_INFLIGHT, inFlight;
|
|
3369
3724
|
var init_service = __esm({
|
|
3370
3725
|
"src/usage/service.ts"() {
|
|
3371
3726
|
"use strict";
|
|
3372
3727
|
init_esm_shims();
|
|
3728
|
+
init_concurrency();
|
|
3373
3729
|
init_apiSurfaces();
|
|
3374
3730
|
init_extract();
|
|
3375
3731
|
USAGE_EXTRACT_BUDGET_MS = 4e3;
|
|
@@ -3494,7 +3850,7 @@ var init_successors2 = __esm({
|
|
|
3494
3850
|
});
|
|
3495
3851
|
|
|
3496
3852
|
// src/db/outcomes.ts
|
|
3497
|
-
import { count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
|
|
3853
|
+
import { and as and4, count, desc as desc3, eq as eq5, inArray as inArray3, sql as sql5 } from "drizzle-orm";
|
|
3498
3854
|
import { alias } from "drizzle-orm/pg-core";
|
|
3499
3855
|
async function recordOutcome(db, outcome) {
|
|
3500
3856
|
await db.insert(recommendationOutcomes).values(outcome);
|
|
@@ -3545,7 +3901,7 @@ async function loadLearnedSuccessors(db, now = Date.now()) {
|
|
|
3545
3901
|
return learnedCache.value;
|
|
3546
3902
|
}
|
|
3547
3903
|
const names2 = [...new Set(observed.map((o) => o.from))];
|
|
3548
|
-
const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(inArray3(packages.name, names2));
|
|
3904
|
+
const rows = await db.select({ name: packages.name, deprecated: packages.deprecated, archived: packages.archived }).from(packages).where(and4(inArray3(packages.name, names2), eq5(packages.ecosystem, DEFAULT_ECOSYSTEM)));
|
|
3549
3905
|
const dead = new Set(rows.filter((r) => r.deprecated || r.archived).map((r) => r.name));
|
|
3550
3906
|
const value = buildLearnedSuccessors(observed, (name) => dead.has(name));
|
|
3551
3907
|
learnedCache = { at: now, value };
|
|
@@ -3559,6 +3915,7 @@ var init_outcomes = __esm({
|
|
|
3559
3915
|
"src/db/outcomes.ts"() {
|
|
3560
3916
|
"use strict";
|
|
3561
3917
|
init_esm_shims();
|
|
3918
|
+
init_types();
|
|
3562
3919
|
init_successors2();
|
|
3563
3920
|
init_schema();
|
|
3564
3921
|
SUCCESS_PREDICATE = sql5`${recommendationOutcomes.accepted} AND (${recommendationOutcomes.buildSignal} IS NULL OR ${recommendationOutcomes.buildSignal} <> 'failed')`;
|
|
@@ -3933,10 +4290,10 @@ function settableKeys() {
|
|
|
3933
4290
|
function applyOverrides(base, sets) {
|
|
3934
4291
|
const next = structuredClone(base);
|
|
3935
4292
|
for (const entry of sets) {
|
|
3936
|
-
const
|
|
3937
|
-
if (
|
|
3938
|
-
const key = entry.slice(0,
|
|
3939
|
-
const value = Number(entry.slice(
|
|
4293
|
+
const eq25 = entry.indexOf("=");
|
|
4294
|
+
if (eq25 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
|
|
4295
|
+
const key = entry.slice(0, eq25).trim();
|
|
4296
|
+
const value = Number(entry.slice(eq25 + 1).trim());
|
|
3940
4297
|
const apply = SETTABLE[key];
|
|
3941
4298
|
if (!apply) {
|
|
3942
4299
|
throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
|
|
@@ -3964,7 +4321,7 @@ function resetWeights() {
|
|
|
3964
4321
|
resetWeightsCache();
|
|
3965
4322
|
return removed;
|
|
3966
4323
|
}
|
|
3967
|
-
var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
4324
|
+
var HEALTH_WEIGHTS, COMPOSITE, RANKING, CONFIDENCE_WEIGHT, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, FIELD, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
3968
4325
|
var init_weights = __esm({
|
|
3969
4326
|
"src/scoring/weights.ts"() {
|
|
3970
4327
|
"use strict";
|
|
@@ -3979,6 +4336,17 @@ var init_weights = __esm({
|
|
|
3979
4336
|
COMPOSITE = {
|
|
3980
4337
|
lambda: 0.35
|
|
3981
4338
|
};
|
|
4339
|
+
RANKING = {
|
|
4340
|
+
relevance: 0.45,
|
|
4341
|
+
composite: 0.35,
|
|
4342
|
+
confidence: 0.2
|
|
4343
|
+
};
|
|
4344
|
+
CONFIDENCE_WEIGHT = {
|
|
4345
|
+
proven: 1,
|
|
4346
|
+
emerging: 0.55,
|
|
4347
|
+
promising: 0.45,
|
|
4348
|
+
unproven: 0.1
|
|
4349
|
+
};
|
|
3982
4350
|
QUALITY_WEIGHTS = {
|
|
3983
4351
|
types: 0.25,
|
|
3984
4352
|
tests: 0.2,
|
|
@@ -4086,12 +4454,44 @@ var init_weights = __esm({
|
|
|
4086
4454
|
proven: {
|
|
4087
4455
|
minWeeklyDownloads: 1e5,
|
|
4088
4456
|
minAgeMonths: 12,
|
|
4089
|
-
|
|
4457
|
+
/**
|
|
4458
|
+
* Recency ceiling for the top tier.
|
|
4459
|
+
*
|
|
4460
|
+
* Was 6 months, which quietly punished stability. Because the stored label
|
|
4461
|
+
* is frozen at ingest, nothing surfaced it until confidence was re-derived
|
|
4462
|
+
* — at which point `p-limit` (273M weekly downloads, ten years old, last
|
|
4463
|
+
* release 6.2 months ago) dropped out of `proven`, along with
|
|
4464
|
+
* `@testing-library/react` at 6.7 months and 44M/wk. A mature utility that
|
|
4465
|
+
* has not needed a release is the most proven kind of package there is.
|
|
4466
|
+
*
|
|
4467
|
+
* Abandonment is already caught three other ways that do not confuse it
|
|
4468
|
+
* with maturity: `deprecated` and `archived` are hard-checked right here,
|
|
4469
|
+
* and the maintenance component of health decays to zero over 24 months
|
|
4470
|
+
* while carrying 35% of the health weight. Recency was the fourth penalty
|
|
4471
|
+
* on the same fact, and the strictest.
|
|
4472
|
+
*/
|
|
4473
|
+
maxLastReleaseMonths: 18
|
|
4090
4474
|
},
|
|
4091
4475
|
emerging: {
|
|
4092
4476
|
minWeeklyDownloads: 5e3,
|
|
4093
4477
|
strongGrowth: 0.5,
|
|
4094
|
-
|
|
4478
|
+
/**
|
|
4479
|
+
* Absolute floor the growth path must also clear.
|
|
4480
|
+
*
|
|
4481
|
+
* Growth alone used to qualify a package for `emerging`, and a percentage
|
|
4482
|
+
* computed on a tiny base is noise: 4 → 17 weekly downloads is +325% and
|
|
4483
|
+
* cleared the bar outright. That is how `@podgres/pg` (17/wk) and
|
|
4484
|
+
* `@bungres/orm` (275/wk) came to wear the same evidence label as a package
|
|
4485
|
+
* with five thousand real installs, and — because `recommend` treats the
|
|
4486
|
+
* label as meaningful — outrank Prisma for "an orm for postgres".
|
|
4487
|
+
*
|
|
4488
|
+
* 1,000/wk is where a 50% swing stops being one CI pipeline changing its
|
|
4489
|
+
* schedule. Below it the growth signal is not measuring adoption.
|
|
4490
|
+
*/
|
|
4491
|
+
minDownloadsForGrowth: 1e3,
|
|
4492
|
+
/** Same reasoning as `proven` above: 9 months demoted `@emotion/react`
|
|
4493
|
+
* (14.7M/wk, last release 9.2 months ago) to `unproven`. */
|
|
4494
|
+
maxLastReleaseMonths: 18
|
|
4095
4495
|
},
|
|
4096
4496
|
/** `promising` (§1): new but intrinsically high-quality, regardless of adoption. */
|
|
4097
4497
|
promising: {
|
|
@@ -4268,7 +4668,7 @@ function computeConfidence(input, now, qualityScore = null) {
|
|
|
4268
4668
|
return "proven";
|
|
4269
4669
|
}
|
|
4270
4670
|
const emergingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.emerging.maxLastReleaseMonths;
|
|
4271
|
-
const emergingAdoptionOk = dl >= CONFIDENCE.emerging.minWeeklyDownloads || growth >= CONFIDENCE.emerging.strongGrowth;
|
|
4671
|
+
const emergingAdoptionOk = dl >= CONFIDENCE.emerging.minWeeklyDownloads || growth >= CONFIDENCE.emerging.strongGrowth && dl >= CONFIDENCE.emerging.minDownloadsForGrowth;
|
|
4272
4672
|
if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
|
|
4273
4673
|
return "emerging";
|
|
4274
4674
|
}
|
|
@@ -4330,7 +4730,7 @@ function buildEmbeddingText(input) {
|
|
|
4330
4730
|
const body = input.summary || input.description || "";
|
|
4331
4731
|
return `${input.name}. ${input.category ?? ""}. ${body}`.trim();
|
|
4332
4732
|
}
|
|
4333
|
-
function
|
|
4733
|
+
function tokenize2(text2) {
|
|
4334
4734
|
return text2.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
4335
4735
|
}
|
|
4336
4736
|
function fnv1a(str3) {
|
|
@@ -4343,7 +4743,7 @@ function fnv1a(str3) {
|
|
|
4343
4743
|
}
|
|
4344
4744
|
function localEmbed(text2, dim2 = EMBEDDING_DIM) {
|
|
4345
4745
|
const vec = new Float64Array(dim2);
|
|
4346
|
-
const tokens =
|
|
4746
|
+
const tokens = tokenize2(text2);
|
|
4347
4747
|
const add = (token, weight) => {
|
|
4348
4748
|
const idx = fnv1a(token) % dim2;
|
|
4349
4749
|
const sign = fnv1a("s:" + token) & 1 ? 1 : -1;
|
|
@@ -4441,7 +4841,7 @@ var init_embeddings = __esm({
|
|
|
4441
4841
|
});
|
|
4442
4842
|
|
|
4443
4843
|
// src/db/alerts.ts
|
|
4444
|
-
import { and as
|
|
4844
|
+
import { and as and5, desc as desc4, eq as eq6 } from "drizzle-orm";
|
|
4445
4845
|
async function insertAlerts(db, rows) {
|
|
4446
4846
|
if (rows.length === 0) return 0;
|
|
4447
4847
|
const inserted = await db.insert(repoAlerts).values(rows).onConflictDoNothing({
|
|
@@ -4453,7 +4853,7 @@ async function listAlerts(db, ownerId, limit = ALERT_FEED_LIMIT) {
|
|
|
4453
4853
|
return db.select().from(repoAlerts).where(eq6(repoAlerts.ownerId, ownerId)).orderBy(desc4(repoAlerts.createdAt)).limit(Math.min(limit, ALERT_FEED_LIMIT));
|
|
4454
4854
|
}
|
|
4455
4855
|
async function deleteAlertsForRepo(db, ownerId, repoId) {
|
|
4456
|
-
await db.delete(repoAlerts).where(
|
|
4856
|
+
await db.delete(repoAlerts).where(and5(eq6(repoAlerts.ownerId, ownerId), eq6(repoAlerts.repoId, repoId)));
|
|
4457
4857
|
}
|
|
4458
4858
|
var ALERT_FEED_LIMIT;
|
|
4459
4859
|
var init_alerts = __esm({
|
|
@@ -4492,7 +4892,7 @@ var init_types2 = __esm({
|
|
|
4492
4892
|
});
|
|
4493
4893
|
|
|
4494
4894
|
// src/db/repos.ts
|
|
4495
|
-
import { and as
|
|
4895
|
+
import { and as and6, eq as eq7, sql as sql6 } from "drizzle-orm";
|
|
4496
4896
|
async function upsertRepos(db, rows) {
|
|
4497
4897
|
if (rows.length === 0) return 0;
|
|
4498
4898
|
await db.insert(repos).values(
|
|
@@ -4525,12 +4925,26 @@ async function reposDeclaring(db, name) {
|
|
|
4525
4925
|
)`
|
|
4526
4926
|
);
|
|
4527
4927
|
}
|
|
4928
|
+
async function ownerForInstallation(db, installationId2) {
|
|
4929
|
+
const rows = await db.select({ ownerId: repos.ownerId }).from(repos).where(eq7(repos.installationId, installationId2)).limit(1);
|
|
4930
|
+
return rows[0]?.ownerId ?? null;
|
|
4931
|
+
}
|
|
4932
|
+
async function deleteReposByInstallation(db, installationId2, fullNames2) {
|
|
4933
|
+
const only = fullNames2 ? new Set(fullNames2) : null;
|
|
4934
|
+
const rows = await db.select({ id: repos.id, ownerId: repos.ownerId, fullName: repos.fullName }).from(repos).where(eq7(repos.installationId, installationId2));
|
|
4935
|
+
let deleted = 0;
|
|
4936
|
+
for (const row of rows) {
|
|
4937
|
+
if (only && !only.has(row.fullName)) continue;
|
|
4938
|
+
if (await deleteRepo(db, row.ownerId, row.id)) deleted++;
|
|
4939
|
+
}
|
|
4940
|
+
return deleted;
|
|
4941
|
+
}
|
|
4528
4942
|
async function getRepo(db, ownerId, id) {
|
|
4529
|
-
const rows = await db.select().from(repos).where(
|
|
4943
|
+
const rows = await db.select().from(repos).where(and6(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).limit(1);
|
|
4530
4944
|
return rows[0] ?? null;
|
|
4531
4945
|
}
|
|
4532
4946
|
async function findPolicyByFullName(db, ownerId, fullName) {
|
|
4533
|
-
const rows = await db.select({ policy: repos.policy }).from(repos).where(
|
|
4947
|
+
const rows = await db.select({ policy: repos.policy }).from(repos).where(and6(eq7(repos.ownerId, ownerId), eq7(repos.fullName, fullName))).limit(1);
|
|
4534
4948
|
return rows[0]?.policy ?? null;
|
|
4535
4949
|
}
|
|
4536
4950
|
async function saveScan(db, id, scan) {
|
|
@@ -4546,12 +4960,12 @@ async function saveScanError(db, id, message) {
|
|
|
4546
4960
|
await db.update(repos).set({ lastScanAt: /* @__PURE__ */ new Date(), lastScanError: message.slice(0, 500) }).where(eq7(repos.id, id));
|
|
4547
4961
|
}
|
|
4548
4962
|
async function setRepoPolicy(db, ownerId, id, policy) {
|
|
4549
|
-
const updated = await db.update(repos).set({ policy }).where(
|
|
4963
|
+
const updated = await db.update(repos).set({ policy }).where(and6(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).returning({ id: repos.id });
|
|
4550
4964
|
return updated.length > 0;
|
|
4551
4965
|
}
|
|
4552
4966
|
async function deleteRepo(db, ownerId, id) {
|
|
4553
4967
|
await deleteAlertsForRepo(db, ownerId, id);
|
|
4554
|
-
const deleted = await db.delete(repos).where(
|
|
4968
|
+
const deleted = await db.delete(repos).where(and6(eq7(repos.ownerId, ownerId), eq7(repos.id, id))).returning({ id: repos.id });
|
|
4555
4969
|
return deleted.length > 0;
|
|
4556
4970
|
}
|
|
4557
4971
|
var init_repos = __esm({
|
|
@@ -4573,8 +4987,8 @@ function buildAttribution(deps, edges, directNames) {
|
|
|
4573
4987
|
}
|
|
4574
4988
|
const parentIds = /* @__PURE__ */ new Map();
|
|
4575
4989
|
for (const edge of edges) {
|
|
4576
|
-
const
|
|
4577
|
-
if (
|
|
4990
|
+
const list2 = parentIds.get(edge.child);
|
|
4991
|
+
if (list2) list2.push(edge.parent);
|
|
4578
4992
|
else parentIds.set(edge.child, [edge.parent]);
|
|
4579
4993
|
}
|
|
4580
4994
|
const informative = edges.some(
|
|
@@ -4686,8 +5100,91 @@ var init_osv = __esm({
|
|
|
4686
5100
|
}
|
|
4687
5101
|
});
|
|
4688
5102
|
|
|
5103
|
+
// src/pipeline/ingestQueue.ts
|
|
5104
|
+
var ingestQueue_exports = {};
|
|
5105
|
+
__export(ingestQueue_exports, {
|
|
5106
|
+
drainIngestQueue: () => drainIngestQueue,
|
|
5107
|
+
enqueueIngest: () => enqueueIngest,
|
|
5108
|
+
ingestQueueDepth: () => ingestQueueDepth,
|
|
5109
|
+
resetIngestQueue: () => resetIngestQueue,
|
|
5110
|
+
runIngest: () => runIngest
|
|
5111
|
+
});
|
|
5112
|
+
function ingestQueueDepth() {
|
|
5113
|
+
return pending.length + inFlight2.size;
|
|
5114
|
+
}
|
|
5115
|
+
async function drainIngestQueue(timeoutMs = 10 * 6e4) {
|
|
5116
|
+
const deadline = Date.now() + timeoutMs;
|
|
5117
|
+
while (ingestQueueDepth() > 0 && Date.now() < deadline) {
|
|
5118
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
5119
|
+
}
|
|
5120
|
+
return ingestQueueDepth();
|
|
5121
|
+
}
|
|
5122
|
+
function resetIngestQueue() {
|
|
5123
|
+
pending.length = 0;
|
|
5124
|
+
inFlight2.clear();
|
|
5125
|
+
queuedNames.clear();
|
|
5126
|
+
owners.clear();
|
|
5127
|
+
active = 0;
|
|
5128
|
+
}
|
|
5129
|
+
function enqueueIngest(db, name, requestedByOwnerId = null) {
|
|
5130
|
+
if (queuedNames.has(name) || inFlight2.has(name)) return;
|
|
5131
|
+
if (pending.length >= MAX_PENDING) {
|
|
5132
|
+
logger.warn(`ingest queue full (${MAX_PENDING}); dropping on-demand request for ${name}`);
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5135
|
+
queuedNames.add(name);
|
|
5136
|
+
owners.set(name, requestedByOwnerId);
|
|
5137
|
+
pending.push(name);
|
|
5138
|
+
pump(db);
|
|
5139
|
+
}
|
|
5140
|
+
function pump(db) {
|
|
5141
|
+
while (active < MAX_CONCURRENT && pending.length > 0) {
|
|
5142
|
+
const name = pending.shift();
|
|
5143
|
+
queuedNames.delete(name);
|
|
5144
|
+
const owner = owners.get(name) ?? null;
|
|
5145
|
+
owners.delete(name);
|
|
5146
|
+
inFlight2.add(name);
|
|
5147
|
+
active += 1;
|
|
5148
|
+
void runIngest(db, name, owner).finally(() => {
|
|
5149
|
+
inFlight2.delete(name);
|
|
5150
|
+
active -= 1;
|
|
5151
|
+
pump(db);
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
async function runIngest(db, name, requestedByOwnerId = null) {
|
|
5156
|
+
try {
|
|
5157
|
+
const row = await syncOnePackage(db, name, { requestedByOwnerId });
|
|
5158
|
+
if (row.confidence && row.confidence !== "unproven") {
|
|
5159
|
+
await ensureSeedEntry(db, name, row.category).catch(() => {
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
return row;
|
|
5163
|
+
} catch (err) {
|
|
5164
|
+
logger.warn(`on-demand ingest failed for ${name}: ${String(err)}`);
|
|
5165
|
+
return null;
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
var MAX_CONCURRENT, MAX_PENDING, pending, inFlight2, queuedNames, owners, active;
|
|
5169
|
+
var init_ingestQueue = __esm({
|
|
5170
|
+
"src/pipeline/ingestQueue.ts"() {
|
|
5171
|
+
"use strict";
|
|
5172
|
+
init_esm_shims();
|
|
5173
|
+
init_logger();
|
|
5174
|
+
init_packages();
|
|
5175
|
+
init_single();
|
|
5176
|
+
MAX_CONCURRENT = 3;
|
|
5177
|
+
MAX_PENDING = 500;
|
|
5178
|
+
pending = [];
|
|
5179
|
+
inFlight2 = /* @__PURE__ */ new Set();
|
|
5180
|
+
queuedNames = /* @__PURE__ */ new Set();
|
|
5181
|
+
owners = /* @__PURE__ */ new Map();
|
|
5182
|
+
active = 0;
|
|
5183
|
+
}
|
|
5184
|
+
});
|
|
5185
|
+
|
|
4689
5186
|
// src/github/drift.ts
|
|
4690
|
-
import { inArray as inArray4, sql as sql7 } from "drizzle-orm";
|
|
5187
|
+
import { and as and7, eq as eq8, inArray as inArray4, sql as sql7 } from "drizzle-orm";
|
|
4691
5188
|
import semver2 from "semver";
|
|
4692
5189
|
function declaredDeps(manifests) {
|
|
4693
5190
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4706,7 +5203,7 @@ function declaredDeps(manifests) {
|
|
|
4706
5203
|
}
|
|
4707
5204
|
return out;
|
|
4708
5205
|
}
|
|
4709
|
-
async function loadIndexed(db, names2) {
|
|
5206
|
+
async function loadIndexed(db, names2, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
4710
5207
|
const out = /* @__PURE__ */ new Map();
|
|
4711
5208
|
for (let i = 0; i < names2.length; i += NAME_CHUNK) {
|
|
4712
5209
|
const rows = await db.select({
|
|
@@ -4714,7 +5211,12 @@ async function loadIndexed(db, names2) {
|
|
|
4714
5211
|
latestVersion: packages.latestVersion,
|
|
4715
5212
|
deprecated: packages.deprecated,
|
|
4716
5213
|
advisories: packages.advisories
|
|
4717
|
-
}).from(packages).where(
|
|
5214
|
+
}).from(packages).where(
|
|
5215
|
+
and7(
|
|
5216
|
+
inArray4(packages.name, names2.slice(i, i + NAME_CHUNK)),
|
|
5217
|
+
eq8(packages.ecosystem, ecosystem)
|
|
5218
|
+
)
|
|
5219
|
+
);
|
|
4718
5220
|
for (const row of rows) {
|
|
4719
5221
|
out.set(row.name, {
|
|
4720
5222
|
latestVersion: row.latestVersion,
|
|
@@ -4730,8 +5232,8 @@ async function loadVersions(db, names2) {
|
|
|
4730
5232
|
for (let i = 0; i < names2.length; i += NAME_CHUNK) {
|
|
4731
5233
|
const rows = await db.select({ name: packageVersions.packageName, version: packageVersions.version }).from(packageVersions).where(inArray4(packageVersions.packageName, names2.slice(i, i + NAME_CHUNK)));
|
|
4732
5234
|
for (const row of rows) {
|
|
4733
|
-
const
|
|
4734
|
-
if (
|
|
5235
|
+
const list2 = out.get(row.name);
|
|
5236
|
+
if (list2) list2.push(row.version);
|
|
4735
5237
|
else out.set(row.name, [row.version]);
|
|
4736
5238
|
}
|
|
4737
5239
|
}
|
|
@@ -4848,7 +5350,15 @@ async function conflictsAtCurrent(db, deps) {
|
|
|
4848
5350
|
}));
|
|
4849
5351
|
return resolveArchitectureCompat(members).slice(0, REPO_DRIFT_DETAIL_CAP);
|
|
4850
5352
|
}
|
|
4851
|
-
|
|
5353
|
+
function queueUntracked(db, names2, ownerId) {
|
|
5354
|
+
if (names2.length === 0) return;
|
|
5355
|
+
void Promise.resolve().then(() => (init_ingestQueue(), ingestQueue_exports)).then(({ enqueueIngest: enqueueIngest2 }) => {
|
|
5356
|
+
for (const name of names2) enqueueIngest2(db, name, ownerId);
|
|
5357
|
+
}).catch((err) => {
|
|
5358
|
+
logger.debug(`untracked-dep enqueue failed: ${String(err)}`);
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
async function computeDrift(db, manifests, resolvedTree = null, requestedByOwnerId = null) {
|
|
4852
5362
|
const declared = declaredDeps(manifests);
|
|
4853
5363
|
const names2 = [...declared.keys()];
|
|
4854
5364
|
const directNames = new Set(names2);
|
|
@@ -4877,6 +5387,11 @@ async function computeDrift(db, manifests, resolvedTree = null) {
|
|
|
4877
5387
|
loadIndexed(db, names2),
|
|
4878
5388
|
loadVersions(db, names2)
|
|
4879
5389
|
]);
|
|
5390
|
+
queueUntracked(
|
|
5391
|
+
db,
|
|
5392
|
+
names2.filter((name) => !indexed.has(name)),
|
|
5393
|
+
requestedByOwnerId
|
|
5394
|
+
);
|
|
4880
5395
|
const deps = [];
|
|
4881
5396
|
for (const [name, entry] of declared) {
|
|
4882
5397
|
const row = indexed.get(name);
|
|
@@ -4906,12 +5421,14 @@ var init_drift = __esm({
|
|
|
4906
5421
|
"src/github/drift.ts"() {
|
|
4907
5422
|
"use strict";
|
|
4908
5423
|
init_esm_shims();
|
|
5424
|
+
init_logger();
|
|
4909
5425
|
init_schema();
|
|
4910
5426
|
init_types2();
|
|
4911
5427
|
init_attribute();
|
|
4912
5428
|
init_osv();
|
|
4913
5429
|
init_members();
|
|
4914
5430
|
init_peerCompat();
|
|
5431
|
+
init_types();
|
|
4915
5432
|
NAME_CHUNK = 500;
|
|
4916
5433
|
}
|
|
4917
5434
|
});
|
|
@@ -4969,14 +5486,6 @@ var init_alerts2 = __esm({
|
|
|
4969
5486
|
}
|
|
4970
5487
|
});
|
|
4971
5488
|
|
|
4972
|
-
// src/core/concurrency.ts
|
|
4973
|
-
var init_concurrency = __esm({
|
|
4974
|
-
"src/core/concurrency.ts"() {
|
|
4975
|
-
"use strict";
|
|
4976
|
-
init_esm_shims();
|
|
4977
|
-
}
|
|
4978
|
-
});
|
|
4979
|
-
|
|
4980
5489
|
// src/core/errors.ts
|
|
4981
5490
|
function truncate(text2, max) {
|
|
4982
5491
|
if (text2.length <= max) return text2;
|
|
@@ -5086,20 +5595,39 @@ var init_runtimeTarget = __esm({
|
|
|
5086
5595
|
}
|
|
5087
5596
|
});
|
|
5088
5597
|
|
|
5598
|
+
// src/db/watch.ts
|
|
5599
|
+
import { eq as eq9 } from "drizzle-orm";
|
|
5600
|
+
async function getWatchCursor(db, id) {
|
|
5601
|
+
const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq9(watchState.id, id)).limit(1);
|
|
5602
|
+
return rows[0]?.seq ?? null;
|
|
5603
|
+
}
|
|
5604
|
+
async function setWatchCursor(db, id, seq) {
|
|
5605
|
+
await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
|
|
5606
|
+
}
|
|
5607
|
+
var init_watch = __esm({
|
|
5608
|
+
"src/db/watch.ts"() {
|
|
5609
|
+
"use strict";
|
|
5610
|
+
init_esm_shims();
|
|
5611
|
+
init_schema();
|
|
5612
|
+
}
|
|
5613
|
+
});
|
|
5614
|
+
|
|
5089
5615
|
// src/pipeline/mineEdges.ts
|
|
5090
|
-
function trackedPairs(nodes, tracked) {
|
|
5616
|
+
function trackedPairs(nodes, tracked, mustTouch) {
|
|
5091
5617
|
const t = nodes.filter((n) => tracked.has(n.name));
|
|
5092
5618
|
const pairs = [];
|
|
5093
5619
|
for (let i = 0; i < t.length; i++) {
|
|
5094
5620
|
for (let j = i + 1; j < t.length; j++) {
|
|
5095
5621
|
if (t[i].name === t[j].name) continue;
|
|
5622
|
+
if (mustTouch && !mustTouch.has(t[i].name) && !mustTouch.has(t[j].name)) continue;
|
|
5096
5623
|
pairs.push(canonicalPair(t[i], t[j]));
|
|
5097
5624
|
}
|
|
5098
5625
|
}
|
|
5099
5626
|
return pairs;
|
|
5100
5627
|
}
|
|
5101
|
-
async function mintObservedPairs(db, nodes, tracked, now, freshWitness) {
|
|
5102
|
-
const pairs = trackedPairs(nodes, tracked);
|
|
5628
|
+
async function mintObservedPairs(db, nodes, tracked, now, freshWitness, mustTouch) {
|
|
5629
|
+
const pairs = trackedPairs(nodes, tracked, mustTouch);
|
|
5630
|
+
if (pairs.length === 0) return 0;
|
|
5103
5631
|
const rows = pairs.map((pair) => ({
|
|
5104
5632
|
...pair,
|
|
5105
5633
|
status: "compatible",
|
|
@@ -5132,6 +5660,7 @@ var init_mineEdges = __esm({
|
|
|
5132
5660
|
init_errors();
|
|
5133
5661
|
init_logger();
|
|
5134
5662
|
init_packages();
|
|
5663
|
+
init_watch();
|
|
5135
5664
|
init_compat();
|
|
5136
5665
|
init_depsDev();
|
|
5137
5666
|
}
|
|
@@ -5208,84 +5737,17 @@ var init_sync = __esm({
|
|
|
5208
5737
|
}
|
|
5209
5738
|
});
|
|
5210
5739
|
|
|
5211
|
-
// src/pipeline/
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
return;
|
|
5217
|
-
}
|
|
5218
|
-
queuedNames.add(name);
|
|
5219
|
-
owners.set(name, requestedByOwnerId);
|
|
5220
|
-
pending.push(name);
|
|
5221
|
-
pump(db);
|
|
5222
|
-
}
|
|
5223
|
-
function pump(db) {
|
|
5224
|
-
while (active < MAX_CONCURRENT && pending.length > 0) {
|
|
5225
|
-
const name = pending.shift();
|
|
5226
|
-
queuedNames.delete(name);
|
|
5227
|
-
const owner = owners.get(name) ?? null;
|
|
5228
|
-
owners.delete(name);
|
|
5229
|
-
inFlight2.add(name);
|
|
5230
|
-
active += 1;
|
|
5231
|
-
void runIngest(db, name, owner).finally(() => {
|
|
5232
|
-
inFlight2.delete(name);
|
|
5233
|
-
active -= 1;
|
|
5234
|
-
pump(db);
|
|
5235
|
-
});
|
|
5236
|
-
}
|
|
5740
|
+
// src/pipeline/single.ts
|
|
5741
|
+
import { and as and8, eq as eq10, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
|
|
5742
|
+
async function getSeedCategory(db, name) {
|
|
5743
|
+
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq10(seedPackages.name, name)).limit(1);
|
|
5744
|
+
return row?.category ?? null;
|
|
5237
5745
|
}
|
|
5238
|
-
async function
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
});
|
|
5244
|
-
}
|
|
5245
|
-
return row;
|
|
5246
|
-
} catch (err) {
|
|
5247
|
-
logger.warn(`on-demand ingest failed for ${name}: ${String(err)}`);
|
|
5248
|
-
return null;
|
|
5249
|
-
}
|
|
5250
|
-
}
|
|
5251
|
-
var MAX_CONCURRENT, MAX_PENDING, pending, inFlight2, queuedNames, owners, active;
|
|
5252
|
-
var init_ingestQueue = __esm({
|
|
5253
|
-
"src/pipeline/ingestQueue.ts"() {
|
|
5254
|
-
"use strict";
|
|
5255
|
-
init_esm_shims();
|
|
5256
|
-
init_logger();
|
|
5257
|
-
init_packages();
|
|
5258
|
-
init_single();
|
|
5259
|
-
MAX_CONCURRENT = 3;
|
|
5260
|
-
MAX_PENDING = 500;
|
|
5261
|
-
pending = [];
|
|
5262
|
-
inFlight2 = /* @__PURE__ */ new Set();
|
|
5263
|
-
queuedNames = /* @__PURE__ */ new Set();
|
|
5264
|
-
owners = /* @__PURE__ */ new Map();
|
|
5265
|
-
active = 0;
|
|
5266
|
-
}
|
|
5267
|
-
});
|
|
5268
|
-
|
|
5269
|
-
// src/pipeline/single.ts
|
|
5270
|
-
import { and as and6, eq as eq8, isNotNull as isNotNull3, sql as sql8 } from "drizzle-orm";
|
|
5271
|
-
function raceTimeout(p, ms) {
|
|
5272
|
-
let timer;
|
|
5273
|
-
return Promise.race([
|
|
5274
|
-
p,
|
|
5275
|
-
new Promise((resolve) => {
|
|
5276
|
-
timer = setTimeout(() => resolve(null), ms);
|
|
5277
|
-
})
|
|
5278
|
-
]).finally(() => clearTimeout(timer));
|
|
5279
|
-
}
|
|
5280
|
-
async function getSeedCategory(db, name) {
|
|
5281
|
-
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq8(seedPackages.name, name)).limit(1);
|
|
5282
|
-
return row?.category ?? null;
|
|
5283
|
-
}
|
|
5284
|
-
async function getCategoryMedianBundle(db, category) {
|
|
5285
|
-
const [row] = await db.select({
|
|
5286
|
-
m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
5287
|
-
}).from(packages).where(and6(eq8(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
|
|
5288
|
-
return row?.m ?? null;
|
|
5746
|
+
async function getCategoryMedianBundle(db, category) {
|
|
5747
|
+
const [row] = await db.select({
|
|
5748
|
+
m: sql8`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
5749
|
+
}).from(packages).where(and8(eq10(packages.category, category), isNotNull3(packages.bundleMinGzipKb)));
|
|
5750
|
+
return row?.m ?? null;
|
|
5289
5751
|
}
|
|
5290
5752
|
async function syncOnePackage(db, name, opts = {}) {
|
|
5291
5753
|
const config = getConfig();
|
|
@@ -5372,7 +5834,7 @@ async function getOrFetchPackage(db, name, opts = {}) {
|
|
|
5372
5834
|
const exists = await npmPackageExists(name);
|
|
5373
5835
|
if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
|
|
5374
5836
|
if (opts.blockMs && opts.blockMs > 0) {
|
|
5375
|
-
const row = await
|
|
5837
|
+
const row = await withBudget(runIngest(db, name, opts.requestedByOwnerId ?? null), opts.blockMs);
|
|
5376
5838
|
if (row) return { row, wasTracked: false, existsOnNpm: true };
|
|
5377
5839
|
return { row: null, wasTracked: false, existsOnNpm: true, queued: true };
|
|
5378
5840
|
}
|
|
@@ -5384,6 +5846,7 @@ var init_single = __esm({
|
|
|
5384
5846
|
"src/pipeline/single.ts"() {
|
|
5385
5847
|
"use strict";
|
|
5386
5848
|
init_esm_shims();
|
|
5849
|
+
init_concurrency();
|
|
5387
5850
|
init_config();
|
|
5388
5851
|
init_logger();
|
|
5389
5852
|
init_collect();
|
|
@@ -5404,7 +5867,12 @@ var init_single = __esm({
|
|
|
5404
5867
|
});
|
|
5405
5868
|
|
|
5406
5869
|
// src/search/recommend.ts
|
|
5407
|
-
import { and as
|
|
5870
|
+
import { and as and9, cosineDistance, eq as eq11, inArray as inArray5, isNotNull as isNotNull4, lte, sql as sql9 } from "drizzle-orm";
|
|
5871
|
+
function rankScore(row, relevance, lambda) {
|
|
5872
|
+
const composite = computeComposite(row.healthScore ?? 0, row.qualityScore, lambda) / 100;
|
|
5873
|
+
const confidence = CONFIDENCE_WEIGHT[row.confidence ?? "unproven"] ?? 0;
|
|
5874
|
+
return RANKING.relevance * relevance + RANKING.composite * composite + RANKING.confidence * confidence;
|
|
5875
|
+
}
|
|
5408
5876
|
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
5409
5877
|
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
5410
5878
|
const [queryVec] = await provider.embed([opts.need]);
|
|
@@ -5420,11 +5888,10 @@ async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
|
5420
5888
|
if (fused.length === 0) return [];
|
|
5421
5889
|
const lambda = loadWeights().composite.lambda;
|
|
5422
5890
|
const maxRrf = Math.max(...fused.map((f) => f.rrf));
|
|
5423
|
-
const ranked = fused.map((f) => {
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5891
|
+
const ranked = fused.map((f) => ({
|
|
5892
|
+
row: f.row,
|
|
5893
|
+
score: rankScore(f.row, maxRrf > 0 ? f.rrf / maxRrf : 0, lambda)
|
|
5894
|
+
})).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5428
5895
|
return ranked.map(({ row }) => toCandidate(row));
|
|
5429
5896
|
}
|
|
5430
5897
|
async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
|
|
@@ -5436,8 +5903,8 @@ async function hybridSearch(db, queryVec, providerId, need, constraints, categor
|
|
|
5436
5903
|
}
|
|
5437
5904
|
function rrfFuse(lists, k = RRF_K) {
|
|
5438
5905
|
const fused = /* @__PURE__ */ new Map();
|
|
5439
|
-
for (const
|
|
5440
|
-
|
|
5906
|
+
for (const list2 of lists) {
|
|
5907
|
+
list2.forEach((row, idx) => {
|
|
5441
5908
|
const contribution = 1 / (k + idx + 1);
|
|
5442
5909
|
const prev = fused.get(row.name);
|
|
5443
5910
|
if (prev) prev.rrf += contribution;
|
|
@@ -5448,14 +5915,14 @@ function rrfFuse(lists, k = RRF_K) {
|
|
|
5448
5915
|
}
|
|
5449
5916
|
function buildConditions(constraints, category) {
|
|
5450
5917
|
const conditions = [];
|
|
5451
|
-
if (category) conditions.push(
|
|
5452
|
-
if (constraints?.license) conditions.push(
|
|
5918
|
+
if (category) conditions.push(eq11(packages.category, category));
|
|
5919
|
+
if (constraints?.license) conditions.push(eq11(packages.license, constraints.license));
|
|
5453
5920
|
if (constraints?.maxBundleKb !== void 0) {
|
|
5454
5921
|
conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
|
|
5455
5922
|
}
|
|
5456
5923
|
if (constraints?.minConfidence) {
|
|
5457
5924
|
const allowed = ["proven", "emerging", "promising", "unproven"].filter(
|
|
5458
|
-
(c) =>
|
|
5925
|
+
(c) => CONFIDENCE_ORDER[c] >= CONFIDENCE_ORDER[constraints.minConfidence]
|
|
5459
5926
|
);
|
|
5460
5927
|
conditions.push(inArray5(packages.confidence, allowed));
|
|
5461
5928
|
}
|
|
@@ -5465,16 +5932,16 @@ async function runVectorQuery(db, queryVec, providerId, constraints, category, p
|
|
|
5465
5932
|
const distance = cosineDistance(packages.embedding, queryVec);
|
|
5466
5933
|
const conditions = [
|
|
5467
5934
|
isNotNull4(packages.embedding),
|
|
5468
|
-
|
|
5935
|
+
eq11(packages.embeddingProvider, providerId),
|
|
5469
5936
|
...buildConditions(constraints, category)
|
|
5470
5937
|
];
|
|
5471
|
-
return db.select(ROW_COLUMNS).from(packages).where(
|
|
5938
|
+
return db.select(ROW_COLUMNS).from(packages).where(and9(...conditions)).orderBy(distance).limit(pool);
|
|
5472
5939
|
}
|
|
5473
5940
|
async function runLexicalQuery(db, need, constraints, category, pool) {
|
|
5474
5941
|
const tsquery = sql9`websearch_to_tsquery('english', ${need})`;
|
|
5475
5942
|
const rank2 = sql9`ts_rank(${packages.searchVector}, ${tsquery})`;
|
|
5476
5943
|
const conditions = [sql9`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
|
|
5477
|
-
return db.select(ROW_COLUMNS).from(packages).where(
|
|
5944
|
+
return db.select(ROW_COLUMNS).from(packages).where(and9(...conditions)).orderBy(sql9`${rank2} desc`).limit(pool);
|
|
5478
5945
|
}
|
|
5479
5946
|
function toCandidate(row) {
|
|
5480
5947
|
return {
|
|
@@ -5487,22 +5954,32 @@ function toCandidate(row) {
|
|
|
5487
5954
|
latestVersion: row.latestVersion,
|
|
5488
5955
|
weeklyDownloads: row.weeklyDownloads,
|
|
5489
5956
|
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
5490
|
-
repoUrl: row.repoUrl
|
|
5957
|
+
repoUrl: row.repoUrl,
|
|
5958
|
+
deprecated: row.deprecated ?? false,
|
|
5959
|
+
archived: row.archived ?? false,
|
|
5960
|
+
advisories: Number(row.advisories ?? 0)
|
|
5491
5961
|
};
|
|
5492
5962
|
}
|
|
5493
5963
|
function buildWhy(row) {
|
|
5964
|
+
const warnings = [];
|
|
5965
|
+
if (row.deprecated) warnings.push("DEPRECATED");
|
|
5966
|
+
if (row.archived) warnings.push("repo archived");
|
|
5967
|
+
if (row.advisories) {
|
|
5968
|
+
warnings.push(`${row.advisories} advisor${row.advisories === 1 ? "y" : "ies"}`);
|
|
5969
|
+
}
|
|
5494
5970
|
const parts = [];
|
|
5495
5971
|
if (row.confidence) parts.push(row.confidence);
|
|
5496
5972
|
if (row.weeklyDownloads) parts.push(`${formatDownloads(row.weeklyDownloads)} weekly downloads`);
|
|
5497
5973
|
if (row.healthScore !== null) parts.push(`health ${row.healthScore}`);
|
|
5498
|
-
|
|
5974
|
+
const rationale = parts.join(", ") || "tracked package";
|
|
5975
|
+
return warnings.length ? `${warnings.join(" + ")} \u2014 ${rationale}` : rationale;
|
|
5499
5976
|
}
|
|
5500
5977
|
function formatDownloads(n) {
|
|
5501
5978
|
if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
|
|
5502
5979
|
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
5503
5980
|
return String(n);
|
|
5504
5981
|
}
|
|
5505
|
-
var
|
|
5982
|
+
var CONFIDENCE_ORDER, RRF_K, ROW_COLUMNS;
|
|
5506
5983
|
var init_recommend = __esm({
|
|
5507
5984
|
"src/search/recommend.ts"() {
|
|
5508
5985
|
"use strict";
|
|
@@ -5512,14 +5989,12 @@ var init_recommend = __esm({
|
|
|
5512
5989
|
init_weights();
|
|
5513
5990
|
init_categoryInference();
|
|
5514
5991
|
init_embeddings();
|
|
5515
|
-
|
|
5992
|
+
CONFIDENCE_ORDER = {
|
|
5516
5993
|
unproven: 0,
|
|
5517
5994
|
promising: 1,
|
|
5518
5995
|
emerging: 2,
|
|
5519
5996
|
proven: 3
|
|
5520
5997
|
};
|
|
5521
|
-
RELEVANCE_WEIGHT = 0.6;
|
|
5522
|
-
COMPOSITE_WEIGHT = 0.4;
|
|
5523
5998
|
RRF_K = 60;
|
|
5524
5999
|
ROW_COLUMNS = {
|
|
5525
6000
|
name: packages.name,
|
|
@@ -5530,7 +6005,14 @@ var init_recommend = __esm({
|
|
|
5530
6005
|
latestVersion: packages.latestVersion,
|
|
5531
6006
|
weeklyDownloads: packages.weeklyDownloads,
|
|
5532
6007
|
lastReleaseAt: packages.lastReleaseAt,
|
|
5533
|
-
repoUrl: packages.repoUrl
|
|
6008
|
+
repoUrl: packages.repoUrl,
|
|
6009
|
+
deprecated: packages.deprecated,
|
|
6010
|
+
archived: packages.archived,
|
|
6011
|
+
// The count, not the array: an agent shortlisting five packages needs to know
|
|
6012
|
+
// that one carries advisories, and `evaluate` is where it reads which. Pulling
|
|
6013
|
+
// the jsonb payload for every candidate would cost tokens on every call for
|
|
6014
|
+
// detail the next call already serves.
|
|
6015
|
+
advisories: sql9`coalesce(jsonb_array_length(${packages.advisories}), 0)`
|
|
5534
6016
|
};
|
|
5535
6017
|
}
|
|
5536
6018
|
});
|
|
@@ -5571,7 +6053,7 @@ function check(policy, pkg, facts) {
|
|
|
5571
6053
|
};
|
|
5572
6054
|
}
|
|
5573
6055
|
if (policy.minConfidence) {
|
|
5574
|
-
if (
|
|
6056
|
+
if (CONFIDENCE_RANK[pkg.confidence] < CONFIDENCE_RANK[policy.minConfidence]) {
|
|
5575
6057
|
return {
|
|
5576
6058
|
name,
|
|
5577
6059
|
rule: "confidence",
|
|
@@ -5581,12 +6063,12 @@ function check(policy, pkg, facts) {
|
|
|
5581
6063
|
}
|
|
5582
6064
|
return null;
|
|
5583
6065
|
}
|
|
5584
|
-
var
|
|
6066
|
+
var CONFIDENCE_RANK;
|
|
5585
6067
|
var init_enforce = __esm({
|
|
5586
6068
|
"src/policy/enforce.ts"() {
|
|
5587
6069
|
"use strict";
|
|
5588
6070
|
init_esm_shims();
|
|
5589
|
-
|
|
6071
|
+
CONFIDENCE_RANK = {
|
|
5590
6072
|
unproven: 0,
|
|
5591
6073
|
promising: 1,
|
|
5592
6074
|
emerging: 2,
|
|
@@ -5612,10 +6094,10 @@ var init_types3 = __esm({
|
|
|
5612
6094
|
});
|
|
5613
6095
|
|
|
5614
6096
|
// src/db/selectionPolicy.ts
|
|
5615
|
-
import { eq as
|
|
6097
|
+
import { and as and10, eq as eq12, inArray as inArray6 } from "drizzle-orm";
|
|
5616
6098
|
async function getSelectionPolicy(db, ownerId) {
|
|
5617
6099
|
if (!ownerId) return DEFAULT_SELECTION_POLICY;
|
|
5618
|
-
const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(
|
|
6100
|
+
const [row] = await db.select({ policy: selectionPolicies.policy }).from(selectionPolicies).where(eq12(selectionPolicies.ownerId, ownerId)).limit(1);
|
|
5619
6101
|
return row ? { ...DEFAULT_SELECTION_POLICY, ...row.policy } : DEFAULT_SELECTION_POLICY;
|
|
5620
6102
|
}
|
|
5621
6103
|
async function setSelectionPolicy(db, ownerId, policy) {
|
|
@@ -5624,14 +6106,14 @@ async function setSelectionPolicy(db, ownerId, policy) {
|
|
|
5624
6106
|
set: { policy, updatedAt: /* @__PURE__ */ new Date() }
|
|
5625
6107
|
});
|
|
5626
6108
|
}
|
|
5627
|
-
async function loadPolicyFacts(db, names2) {
|
|
6109
|
+
async function loadPolicyFacts(db, names2, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
5628
6110
|
const out = /* @__PURE__ */ new Map();
|
|
5629
6111
|
if (names2.length === 0) return out;
|
|
5630
6112
|
const rows = await db.select({
|
|
5631
6113
|
name: packages.name,
|
|
5632
6114
|
license: packages.license,
|
|
5633
6115
|
deprecated: packages.deprecated
|
|
5634
|
-
}).from(packages).where(inArray6(packages.name, names2));
|
|
6116
|
+
}).from(packages).where(and10(inArray6(packages.name, names2), eq12(packages.ecosystem, ecosystem)));
|
|
5635
6117
|
for (const row of rows) {
|
|
5636
6118
|
out.set(row.name, { license: row.license, deprecated: row.deprecated });
|
|
5637
6119
|
}
|
|
@@ -5642,6 +6124,7 @@ var init_selectionPolicy = __esm({
|
|
|
5642
6124
|
"use strict";
|
|
5643
6125
|
init_esm_shims();
|
|
5644
6126
|
init_schema();
|
|
6127
|
+
init_types();
|
|
5645
6128
|
init_types3();
|
|
5646
6129
|
}
|
|
5647
6130
|
});
|
|
@@ -6060,6 +6543,12 @@ async function handleEvaluate(db, input, ownerId = null) {
|
|
|
6060
6543
|
);
|
|
6061
6544
|
return { ...evaluated, policy: exclusion ? { allowed: false, ...exclusion } : { allowed: true } };
|
|
6062
6545
|
}
|
|
6546
|
+
function comparisonNote(notFound, pending2) {
|
|
6547
|
+
return [
|
|
6548
|
+
notFound.length ? `Not found on the npm registry: ${notFound.join(", ")}. Check the name(s) before installing \u2014 these do not exist.` : null,
|
|
6549
|
+
pending2.length ? `\u{1F389} You're the first to add ${pending2.join(", ")} to lurq's registry! They're being scored now; retry shortly for the full comparison.` : null
|
|
6550
|
+
].filter(Boolean).join(" ");
|
|
6551
|
+
}
|
|
6063
6552
|
async function handleCompare(db, input, ownerId = null) {
|
|
6064
6553
|
const out = await cached2(
|
|
6065
6554
|
"cmp",
|
|
@@ -6069,17 +6558,28 @@ async function handleCompare(db, input, ownerId = null) {
|
|
|
6069
6558
|
input.packages.map((name) => getOrFetchPackage(db, name, { requestedByOwnerId: ownerId }))
|
|
6070
6559
|
);
|
|
6071
6560
|
const rows = results.map((r) => r.row).filter((row) => row !== null).map((row) => rowToEvaluate(row)).sort((a, b) => b.healthScore - a.healthScore);
|
|
6072
|
-
const
|
|
6561
|
+
const missing2 = [];
|
|
6562
|
+
const notFound = [];
|
|
6563
|
+
const pending2 = [];
|
|
6564
|
+
input.packages.forEach((name, i) => {
|
|
6565
|
+
const result = results[i];
|
|
6566
|
+
if (result.row) return;
|
|
6567
|
+
missing2.push(name);
|
|
6568
|
+
(result.existsOnNpm ? pending2 : notFound).push(name);
|
|
6569
|
+
});
|
|
6073
6570
|
return {
|
|
6074
6571
|
dataAsOf: await latestDataAsOf(db),
|
|
6075
6572
|
rows,
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
} : {}
|
|
6573
|
+
// `missing` stays the union, in the caller's own order, so a client
|
|
6574
|
+
// pinned to an older lurq keeps printing its "not found" line.
|
|
6575
|
+
...missing2.length ? { missing: missing2 } : {},
|
|
6576
|
+
...notFound.length ? { notFound } : {},
|
|
6577
|
+
...pending2.length ? { pending: pending2 } : {},
|
|
6578
|
+
...missing2.length ? { note: comparisonNote(notFound, pending2) } : {}
|
|
6080
6579
|
};
|
|
6081
6580
|
},
|
|
6082
|
-
// Don't cache a
|
|
6581
|
+
// Don't cache a miss: a pending ingest lands within seconds, and a name that
|
|
6582
|
+
// is not on npm today may be published tomorrow.
|
|
6083
6583
|
{ skipCache: (r) => Boolean(r.missing?.length) }
|
|
6084
6584
|
);
|
|
6085
6585
|
out.rows = out.rows.map(refreshStale);
|
|
@@ -6169,14 +6669,18 @@ async function handleVerify(db, input, ownerId = null) {
|
|
|
6169
6669
|
};
|
|
6170
6670
|
}
|
|
6171
6671
|
async function resolveVersion(db, name, requested) {
|
|
6172
|
-
if (requested)
|
|
6672
|
+
if (requested) {
|
|
6673
|
+
const exists = await npmVersionExists(name, requested);
|
|
6674
|
+
return exists === false ? { version: null, unpublished: true } : { version: requested };
|
|
6675
|
+
}
|
|
6173
6676
|
const row = await getPackageByName(db, name);
|
|
6174
|
-
if (row?.latestVersion) return row.latestVersion;
|
|
6677
|
+
if (row?.latestVersion) return { version: row.latestVersion };
|
|
6175
6678
|
const reg = await fetchNpmRegistry(name).catch(() => null);
|
|
6176
|
-
return reg?.latestVersion ?? null;
|
|
6679
|
+
return { version: reg?.latestVersion ?? null };
|
|
6177
6680
|
}
|
|
6178
6681
|
async function handleUsage(db, input) {
|
|
6179
|
-
const
|
|
6682
|
+
const resolved = await resolveVersion(db, input.package, input.version);
|
|
6683
|
+
const version = resolved.version;
|
|
6180
6684
|
if (!version) {
|
|
6181
6685
|
return {
|
|
6182
6686
|
package: input.package,
|
|
@@ -6184,7 +6688,8 @@ async function handleUsage(db, input) {
|
|
|
6184
6688
|
surface: null,
|
|
6185
6689
|
available: false,
|
|
6186
6690
|
engines: null,
|
|
6187
|
-
note: `Could not resolve a version for "${input.package}" on npm
|
|
6691
|
+
note: resolved.unpublished ? `"${input.package}" has no published version ${input.version}. That release does not exist on the npm registry \u2014 check the version before writing code against it.` : `Could not resolve a version for "${input.package}" on npm.`,
|
|
6692
|
+
...resolved.unpublished ? { unpublishedVersion: input.version } : {}
|
|
6188
6693
|
};
|
|
6189
6694
|
}
|
|
6190
6695
|
const deadline = Date.now() + USAGE_EXTRACT_BUDGET_MS;
|
|
@@ -6207,6 +6712,10 @@ async function handleUsage(db, input) {
|
|
|
6207
6712
|
budgetMs: Math.max(0, deadline - Date.now())
|
|
6208
6713
|
});
|
|
6209
6714
|
if (known) out.delta = { ...diffSurface(known, surface), fromVersion: input.knownVersion };
|
|
6715
|
+
else {
|
|
6716
|
+
const knownExists = await npmVersionExists(input.package, input.knownVersion);
|
|
6717
|
+
out.deltaNote = knownExists === false ? `No delta: "${input.package}" has no published version ${input.knownVersion}. That release does not exist on the npm registry.` : `No delta: no API surface extracted for ${input.package}@${input.knownVersion} yet. Retry shortly, or compare against a version that has been indexed.`;
|
|
6718
|
+
}
|
|
6210
6719
|
}
|
|
6211
6720
|
return out;
|
|
6212
6721
|
}
|
|
@@ -6262,7 +6771,8 @@ __export(remote_exports, {
|
|
|
6262
6771
|
apiKey: () => apiKey,
|
|
6263
6772
|
callTool: () => callTool,
|
|
6264
6773
|
endpoint: () => endpoint,
|
|
6265
|
-
fetchUpgradePlan: () => fetchUpgradePlan
|
|
6774
|
+
fetchUpgradePlan: () => fetchUpgradePlan,
|
|
6775
|
+
reportUpgradeRuns: () => reportUpgradeRuns
|
|
6266
6776
|
});
|
|
6267
6777
|
function endpoint(override) {
|
|
6268
6778
|
const base = resolveEndpoint(override) ?? DEFAULT_ENDPOINT;
|
|
@@ -6356,6 +6866,9 @@ function fetchUpgradePlan(deps, opts = {}) {
|
|
|
6356
6866
|
const body = opts.repo ? { deps, repo: opts.repo } : { deps };
|
|
6357
6867
|
return post("/upgrade-plan", body, opts);
|
|
6358
6868
|
}
|
|
6869
|
+
function reportUpgradeRuns(runs, opts = {}) {
|
|
6870
|
+
return post("/upgrade-runs", { runs }, opts);
|
|
6871
|
+
}
|
|
6359
6872
|
var RemoteError;
|
|
6360
6873
|
var init_remote = __esm({
|
|
6361
6874
|
"src/cli/remote.ts"() {
|
|
@@ -6449,7 +6962,7 @@ var init_optimize = __esm({
|
|
|
6449
6962
|
});
|
|
6450
6963
|
|
|
6451
6964
|
// src/mcp/diagram.ts
|
|
6452
|
-
import { inArray as inArray7 } from "drizzle-orm";
|
|
6965
|
+
import { and as and11, eq as eq13, inArray as inArray7 } from "drizzle-orm";
|
|
6453
6966
|
function layerFor(item) {
|
|
6454
6967
|
if (!item.category) return UNCLASSIFIED;
|
|
6455
6968
|
if (item.category === "framework" && item.runtimeTarget === "node") return "Backend";
|
|
@@ -6462,19 +6975,19 @@ function buildMermaid(items) {
|
|
|
6462
6975
|
const byLayer = /* @__PURE__ */ new Map();
|
|
6463
6976
|
for (const item of items) {
|
|
6464
6977
|
const layer = layerFor(item);
|
|
6465
|
-
const
|
|
6466
|
-
|
|
6467
|
-
byLayer.set(layer,
|
|
6978
|
+
const list2 = byLayer.get(layer) ?? [];
|
|
6979
|
+
list2.push(item);
|
|
6980
|
+
byLayer.set(layer, list2);
|
|
6468
6981
|
}
|
|
6469
6982
|
const lines = ["flowchart TD", " user([User])"];
|
|
6470
6983
|
const ids = /* @__PURE__ */ new Map();
|
|
6471
6984
|
let counter = 0;
|
|
6472
6985
|
const repNode = /* @__PURE__ */ new Map();
|
|
6473
6986
|
const emitLayer = (layer) => {
|
|
6474
|
-
const
|
|
6475
|
-
if (!
|
|
6987
|
+
const list2 = byLayer.get(layer);
|
|
6988
|
+
if (!list2?.length) return;
|
|
6476
6989
|
lines.push(` subgraph ${layer.replace(/\s+/g, "_")}["${layer}"]`);
|
|
6477
|
-
for (const item of
|
|
6990
|
+
for (const item of list2) {
|
|
6478
6991
|
const id = nodeId(item.label, counter++);
|
|
6479
6992
|
ids.set(item.label, id);
|
|
6480
6993
|
if (!repNode.has(layer)) repNode.set(layer, id);
|
|
@@ -6506,7 +7019,9 @@ async function handleDiagram(db, input) {
|
|
|
6506
7019
|
name: packages.name,
|
|
6507
7020
|
category: packages.category,
|
|
6508
7021
|
runtimeTarget: packages.runtimeTarget
|
|
6509
|
-
}).from(packages).where(
|
|
7022
|
+
}).from(packages).where(
|
|
7023
|
+
and11(inArray7(packages.name, input.stack), eq13(packages.ecosystem, DEFAULT_ECOSYSTEM))
|
|
7024
|
+
);
|
|
6510
7025
|
const known = new Map(rows.map((r) => [r.name, r]));
|
|
6511
7026
|
const items = input.stack.map((name) => {
|
|
6512
7027
|
const row = known.get(name);
|
|
@@ -6535,6 +7050,7 @@ var init_diagram = __esm({
|
|
|
6535
7050
|
"src/mcp/diagram.ts"() {
|
|
6536
7051
|
"use strict";
|
|
6537
7052
|
init_esm_shims();
|
|
7053
|
+
init_types();
|
|
6538
7054
|
init_categoryInference();
|
|
6539
7055
|
init_runtimeTarget();
|
|
6540
7056
|
init_schema();
|
|
@@ -6578,7 +7094,7 @@ __export(plan_exports, {
|
|
|
6578
7094
|
resolvePins: () => resolvePins
|
|
6579
7095
|
});
|
|
6580
7096
|
import { createHash as createHash4 } from "crypto";
|
|
6581
|
-
import { inArray as inArray8 } from "drizzle-orm";
|
|
7097
|
+
import { and as and12, eq as eq14, inArray as inArray8 } from "drizzle-orm";
|
|
6582
7098
|
function packageToCandidate(row) {
|
|
6583
7099
|
return {
|
|
6584
7100
|
name: row.name,
|
|
@@ -6590,17 +7106,24 @@ function packageToCandidate(row) {
|
|
|
6590
7106
|
latestVersion: row.latestVersion,
|
|
6591
7107
|
weeklyDownloads: row.weeklyDownloads,
|
|
6592
7108
|
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
6593
|
-
repoUrl: row.repoUrl
|
|
7109
|
+
repoUrl: row.repoUrl,
|
|
7110
|
+
// A pin is the one slot lurq did not choose, which makes these MORE worth
|
|
7111
|
+
// saying, not less: the user picked it, so nobody else is going to notice
|
|
7112
|
+
// that it has been deprecated since they did.
|
|
7113
|
+
deprecated: row.deprecated,
|
|
7114
|
+
archived: row.archived,
|
|
7115
|
+
advisories: row.advisories?.length ?? 0
|
|
6594
7116
|
};
|
|
6595
7117
|
}
|
|
6596
7118
|
async function resolvePins(db, using, recommendedNames, requestedByOwnerId = null) {
|
|
6597
7119
|
const slots = [];
|
|
6598
|
-
const
|
|
7120
|
+
const notFound = [];
|
|
7121
|
+
const pending2 = [];
|
|
6599
7122
|
for (const name of new Set(using ?? [])) {
|
|
6600
7123
|
if (recommendedNames.has(name)) continue;
|
|
6601
|
-
const { row } = await getOrFetchPackage(db, name, { requestedByOwnerId });
|
|
7124
|
+
const { row, existsOnNpm } = await getOrFetchPackage(db, name, { requestedByOwnerId });
|
|
6602
7125
|
if (!row) {
|
|
6603
|
-
|
|
7126
|
+
(existsOnNpm ? pending2 : notFound).push(name);
|
|
6604
7127
|
continue;
|
|
6605
7128
|
}
|
|
6606
7129
|
slots.push({
|
|
@@ -6613,7 +7136,7 @@ async function resolvePins(db, using, recommendedNames, requestedByOwnerId = nul
|
|
|
6613
7136
|
note: "pinned by you"
|
|
6614
7137
|
});
|
|
6615
7138
|
}
|
|
6616
|
-
return { slots,
|
|
7139
|
+
return { slots, notFound, pending: pending2 };
|
|
6617
7140
|
}
|
|
6618
7141
|
async function handlePlan(db, input, ownerId = null) {
|
|
6619
7142
|
const optimize = input.optimize ?? "balanced";
|
|
@@ -6670,17 +7193,20 @@ async function handlePlan(db, input, ownerId = null) {
|
|
|
6670
7193
|
const recommendedNames = new Set(
|
|
6671
7194
|
recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
|
|
6672
7195
|
);
|
|
6673
|
-
const {
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
);
|
|
7196
|
+
const {
|
|
7197
|
+
slots: pinnedSlots,
|
|
7198
|
+
notFound: pinsNotFound,
|
|
7199
|
+
pending: pinsPending
|
|
7200
|
+
} = await resolvePins(db, input.using, recommendedNames, ownerId);
|
|
6679
7201
|
const slots = [...pinnedSlots, ...recSlots];
|
|
6680
7202
|
const compatibility = await resolveCompat(db, slots);
|
|
6681
7203
|
const unmatched = [
|
|
6682
7204
|
...slots.filter((s) => !s.recommended).map((s) => s.need),
|
|
6683
|
-
...
|
|
7205
|
+
...pinsNotFound.map((n) => `${n} (pinned, but not on the npm registry)`),
|
|
7206
|
+
// A pin that exists and is merely being ingested is not an unmatched need —
|
|
7207
|
+
// it just has no scores yet. Saying so keeps the user from "fixing" a name
|
|
7208
|
+
// that was correct all along.
|
|
7209
|
+
...pinsPending.map((n) => `${n} (pinned; being scored now, retry shortly)`)
|
|
6684
7210
|
];
|
|
6685
7211
|
const mermaid = buildMermaid(
|
|
6686
7212
|
slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
|
|
@@ -6799,7 +7325,7 @@ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
|
|
|
6799
7325
|
async function bundleSizes(db, candidates) {
|
|
6800
7326
|
const names2 = [...new Set(candidates.map((c) => c.name))];
|
|
6801
7327
|
if (names2.length === 0) return /* @__PURE__ */ new Map();
|
|
6802
|
-
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray8(packages.name, names2));
|
|
7328
|
+
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(and12(inArray8(packages.name, names2), eq14(packages.ecosystem, DEFAULT_ECOSYSTEM)));
|
|
6803
7329
|
return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
|
|
6804
7330
|
}
|
|
6805
7331
|
async function decompose(document) {
|
|
@@ -6871,6 +7397,7 @@ var init_plan = __esm({
|
|
|
6871
7397
|
"src/mcp/plan.ts"() {
|
|
6872
7398
|
"use strict";
|
|
6873
7399
|
init_esm_shims();
|
|
7400
|
+
init_types();
|
|
6874
7401
|
init_config();
|
|
6875
7402
|
init_http();
|
|
6876
7403
|
init_check();
|
|
@@ -6986,17 +7513,20 @@ var init_planView = __esm({
|
|
|
6986
7513
|
}
|
|
6987
7514
|
});
|
|
6988
7515
|
|
|
6989
|
-
// src/db/
|
|
6990
|
-
import { eq as
|
|
6991
|
-
async function
|
|
6992
|
-
const rows = await db.select({
|
|
6993
|
-
return rows
|
|
7516
|
+
// src/db/discovery.ts
|
|
7517
|
+
import { eq as eq15 } from "drizzle-orm";
|
|
7518
|
+
async function getQueuedNames(db) {
|
|
7519
|
+
const rows = await db.select({ name: discoveryQueue.name }).from(discoveryQueue);
|
|
7520
|
+
return new Set(rows.map((r) => r.name));
|
|
6994
7521
|
}
|
|
6995
|
-
async function
|
|
6996
|
-
|
|
7522
|
+
async function enqueueCandidates(db, candidates) {
|
|
7523
|
+
if (candidates.length === 0) return 0;
|
|
7524
|
+
const rows = candidates.map((c) => ({ name: c.name, discoveredVia: c.via }));
|
|
7525
|
+
const inserted = await db.insert(discoveryQueue).values(rows).onConflictDoNothing({ target: discoveryQueue.name }).returning({ id: discoveryQueue.id });
|
|
7526
|
+
return inserted.length;
|
|
6997
7527
|
}
|
|
6998
|
-
var
|
|
6999
|
-
"src/db/
|
|
7528
|
+
var init_discovery = __esm({
|
|
7529
|
+
"src/db/discovery.ts"() {
|
|
7000
7530
|
"use strict";
|
|
7001
7531
|
init_esm_shims();
|
|
7002
7532
|
init_schema();
|
|
@@ -7006,56 +7536,104 @@ var init_watch = __esm({
|
|
|
7006
7536
|
// src/pipeline/watch.ts
|
|
7007
7537
|
var watch_exports = {};
|
|
7008
7538
|
__export(watch_exports, {
|
|
7009
|
-
|
|
7539
|
+
fetchHeadSeq: () => fetchHeadSeq,
|
|
7540
|
+
parseChange: () => parseChange,
|
|
7541
|
+
parseChangesPage: () => parseChangesPage,
|
|
7542
|
+
routeChange: () => routeChange,
|
|
7010
7543
|
watchNpmChanges: () => watchNpmChanges
|
|
7011
7544
|
});
|
|
7012
|
-
function
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
}
|
|
7545
|
+
function parseChange(obj) {
|
|
7546
|
+
if (typeof obj !== "object" || obj === null) return null;
|
|
7547
|
+
const rec = obj;
|
|
7548
|
+
if (typeof rec.id !== "string" || rec.seq == null) return null;
|
|
7549
|
+
if (typeof rec.seq !== "number" && typeof rec.seq !== "string") return null;
|
|
7550
|
+
return { seq: rec.seq, id: rec.id, deleted: Boolean(rec.deleted) };
|
|
7551
|
+
}
|
|
7552
|
+
function parseChangesPage(body) {
|
|
7553
|
+
if (typeof body !== "object" || body === null) return { changes: [], lastSeq: null };
|
|
7554
|
+
const { results, last_seq: lastSeq } = body;
|
|
7555
|
+
const changes = Array.isArray(results) ? results.map(parseChange).filter((c) => c !== null) : [];
|
|
7556
|
+
return {
|
|
7557
|
+
changes,
|
|
7558
|
+
lastSeq: typeof lastSeq === "number" || typeof lastSeq === "string" ? String(lastSeq) : null
|
|
7559
|
+
};
|
|
7560
|
+
}
|
|
7561
|
+
function routeChange(change2, tracked, queued) {
|
|
7562
|
+
if (change2.deleted) return "skip";
|
|
7563
|
+
if (tracked.has(change2.id)) return "resync";
|
|
7564
|
+
if (queued.has(change2.id)) return "skip";
|
|
7565
|
+
return "enqueue";
|
|
7566
|
+
}
|
|
7567
|
+
async function getJson(url, fetchImpl, signal) {
|
|
7568
|
+
const res = await fetchImpl(url, { signal });
|
|
7569
|
+
if (!res.ok) throw new Error(`feed responded ${res.status}`);
|
|
7570
|
+
return res.json();
|
|
7571
|
+
}
|
|
7572
|
+
async function fetchHeadSeq(fetchImpl = fetch, signal) {
|
|
7573
|
+
const body = await getJson(`${FEED_URL}?descending=true&limit=1`, fetchImpl, signal);
|
|
7574
|
+
return parseChangesPage(body).lastSeq;
|
|
7575
|
+
}
|
|
7576
|
+
async function loadSeenSets(db) {
|
|
7577
|
+
const [tracked, queued] = await Promise.all([getAllPackageNames(db), getQueuedNames(db)]);
|
|
7578
|
+
return [new Set(tracked), queued];
|
|
7022
7579
|
}
|
|
7023
7580
|
async function watchNpmChanges(db, opts = {}) {
|
|
7024
|
-
const { signal } = opts;
|
|
7581
|
+
const { signal, fetchImpl = fetch } = opts;
|
|
7025
7582
|
let backoff2 = 1e3;
|
|
7583
|
+
let [tracked, queued] = await loadSeenSets(db);
|
|
7584
|
+
let seenAt = Date.now();
|
|
7585
|
+
let cursor = await getWatchCursor(db, FEED_ID) ?? opts.since ?? await fetchHeadSeq(fetchImpl, signal).catch(() => null);
|
|
7586
|
+
if (cursor == null) {
|
|
7587
|
+
logger.warn("watch: could not establish a starting sequence; giving up.");
|
|
7588
|
+
return;
|
|
7589
|
+
}
|
|
7590
|
+
logger.info(
|
|
7591
|
+
`watch: following from seq=${cursor} (${tracked.size} tracked, ${queued.size} queued)`
|
|
7592
|
+
);
|
|
7026
7593
|
while (!signal?.aborted) {
|
|
7027
|
-
let tracked = new Set(await getAllPackageNames(db));
|
|
7028
|
-
let trackedAt = Date.now();
|
|
7029
|
-
const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
|
|
7030
|
-
const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
|
|
7031
|
-
logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
|
|
7032
7594
|
try {
|
|
7033
|
-
const
|
|
7034
|
-
|
|
7595
|
+
const url = `${FEED_URL}?since=${encodeURIComponent(cursor)}&limit=${PAGE_SIZE}`;
|
|
7596
|
+
const { changes, lastSeq } = parseChangesPage(await getJson(url, fetchImpl, signal));
|
|
7035
7597
|
backoff2 = 1e3;
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
if (
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7598
|
+
if (Date.now() - seenAt > TRACKED_REFRESH_MS) {
|
|
7599
|
+
[tracked, queued] = await loadSeenSets(db);
|
|
7600
|
+
seenAt = Date.now();
|
|
7601
|
+
}
|
|
7602
|
+
const buffered = [];
|
|
7603
|
+
const flush = async () => {
|
|
7604
|
+
if (buffered.length === 0) return;
|
|
7605
|
+
const batch = buffered.map((name) => ({ name, via: "npm-changes" }));
|
|
7606
|
+
const inserted = await enqueueCandidates(db, batch).catch((err) => {
|
|
7607
|
+
logger.warn(`watch: enqueue of ${batch.length} candidate(s) failed: ${String(err)}`);
|
|
7608
|
+
return 0;
|
|
7609
|
+
});
|
|
7610
|
+
if (inserted > 0) logger.info(`watch: queued ${inserted} new discovery candidate(s)`);
|
|
7611
|
+
buffered.length = 0;
|
|
7612
|
+
};
|
|
7613
|
+
let resynced = 0;
|
|
7614
|
+
for (const change2 of changes) {
|
|
7615
|
+
if (signal?.aborted) break;
|
|
7616
|
+
const route = routeChange(change2, tracked, queued);
|
|
7617
|
+
if (route === "resync") {
|
|
7618
|
+
await syncOnePackage(db, change2.id).catch(
|
|
7619
|
+
(err) => logger.warn(`watch: re-sync failed for ${change2.id}: ${String(err)}`)
|
|
7050
7620
|
);
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7621
|
+
resynced++;
|
|
7622
|
+
} else if (route === "enqueue") {
|
|
7623
|
+
queued.add(change2.id);
|
|
7624
|
+
buffered.push(change2.id);
|
|
7625
|
+
if (buffered.length >= ENQUEUE_BATCH) await flush();
|
|
7056
7626
|
}
|
|
7057
7627
|
}
|
|
7058
|
-
|
|
7628
|
+
await flush();
|
|
7629
|
+
if (lastSeq !== null && lastSeq !== cursor) {
|
|
7630
|
+
cursor = lastSeq;
|
|
7631
|
+
await setWatchCursor(db, FEED_ID, cursor);
|
|
7632
|
+
}
|
|
7633
|
+
if (changes.length > 0) {
|
|
7634
|
+
logger.info(`watch: ${changes.length} change(s), ${resynced} re-synced, seq=${cursor}`);
|
|
7635
|
+
}
|
|
7636
|
+
if (changes.length < PAGE_SIZE) await sleep(IDLE_POLL_MS, signal);
|
|
7059
7637
|
} catch (err) {
|
|
7060
7638
|
if (signal?.aborted) break;
|
|
7061
7639
|
logger.warn(`watch: ${String(err)}, retrying in ${backoff2}ms`);
|
|
@@ -7063,54 +7641,38 @@ async function watchNpmChanges(db, opts = {}) {
|
|
|
7063
7641
|
backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
|
|
7064
7642
|
}
|
|
7065
7643
|
}
|
|
7066
|
-
|
|
7067
|
-
async function* ndjsonLines(body, signal) {
|
|
7068
|
-
const reader = body.getReader();
|
|
7069
|
-
const decoder = new TextDecoder();
|
|
7070
|
-
let buffer = "";
|
|
7071
|
-
try {
|
|
7072
|
-
while (!signal?.aborted) {
|
|
7073
|
-
const { value, done } = await reader.read();
|
|
7074
|
-
if (done) break;
|
|
7075
|
-
buffer += decoder.decode(value, { stream: true });
|
|
7076
|
-
let nl;
|
|
7077
|
-
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
7078
|
-
yield buffer.slice(0, nl);
|
|
7079
|
-
buffer = buffer.slice(nl + 1);
|
|
7080
|
-
}
|
|
7081
|
-
}
|
|
7082
|
-
} finally {
|
|
7083
|
-
reader.releaseLock();
|
|
7084
|
-
}
|
|
7644
|
+
logger.info("watch: stopped.");
|
|
7085
7645
|
}
|
|
7086
7646
|
function sleep(ms, signal) {
|
|
7087
|
-
return new Promise((
|
|
7088
|
-
const timer = setTimeout(
|
|
7647
|
+
return new Promise((resolve2) => {
|
|
7648
|
+
const timer = setTimeout(resolve2, ms);
|
|
7089
7649
|
signal?.addEventListener(
|
|
7090
7650
|
"abort",
|
|
7091
7651
|
() => {
|
|
7092
7652
|
clearTimeout(timer);
|
|
7093
|
-
|
|
7653
|
+
resolve2();
|
|
7094
7654
|
},
|
|
7095
7655
|
{ once: true }
|
|
7096
7656
|
);
|
|
7097
7657
|
});
|
|
7098
7658
|
}
|
|
7099
|
-
var FEED_ID, FEED_URL,
|
|
7659
|
+
var FEED_ID, FEED_URL, PAGE_SIZE, IDLE_POLL_MS, TRACKED_REFRESH_MS, MAX_BACKOFF_MS, ENQUEUE_BATCH;
|
|
7100
7660
|
var init_watch2 = __esm({
|
|
7101
7661
|
"src/pipeline/watch.ts"() {
|
|
7102
7662
|
"use strict";
|
|
7103
7663
|
init_esm_shims();
|
|
7104
7664
|
init_logger();
|
|
7665
|
+
init_discovery();
|
|
7105
7666
|
init_packages();
|
|
7106
7667
|
init_watch();
|
|
7107
7668
|
init_single();
|
|
7108
7669
|
FEED_ID = "npm-changes";
|
|
7109
7670
|
FEED_URL = "https://replicate.npmjs.com/_changes";
|
|
7110
|
-
|
|
7671
|
+
PAGE_SIZE = 1e3;
|
|
7672
|
+
IDLE_POLL_MS = 3e4;
|
|
7111
7673
|
TRACKED_REFRESH_MS = 5 * 6e4;
|
|
7112
|
-
CHECKPOINT_EVERY = 200;
|
|
7113
7674
|
MAX_BACKOFF_MS = 3e4;
|
|
7675
|
+
ENQUEUE_BATCH = 200;
|
|
7114
7676
|
}
|
|
7115
7677
|
});
|
|
7116
7678
|
|
|
@@ -7711,6 +8273,7 @@ var init_compat2 = __esm({
|
|
|
7711
8273
|
var commands_exports = {};
|
|
7712
8274
|
__export(commands_exports, {
|
|
7713
8275
|
indexSource: () => indexSource,
|
|
8276
|
+
runCan: () => runCan,
|
|
7714
8277
|
runCompare: () => runCompare,
|
|
7715
8278
|
runCompat: () => runCompat,
|
|
7716
8279
|
runCompatBackfill: () => runCompatBackfill,
|
|
@@ -7749,6 +8312,14 @@ async function fromIndex(tool, args, local) {
|
|
|
7749
8312
|
const { callTool: callTool2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
7750
8313
|
return callTool2(tool, args);
|
|
7751
8314
|
}
|
|
8315
|
+
function candidateFlags(c) {
|
|
8316
|
+
const flags = [
|
|
8317
|
+
c.deprecated && red("deprecated"),
|
|
8318
|
+
c.archived && yellow("archived"),
|
|
8319
|
+
c.advisories ? yellow(`${c.advisories} advisory`) : null
|
|
8320
|
+
].filter(Boolean);
|
|
8321
|
+
return flags.length ? flags.join(" ") : dim("\u2014");
|
|
8322
|
+
}
|
|
7752
8323
|
async function runRecommend(need, opts) {
|
|
7753
8324
|
if (opts.category && !isCategory(opts.category)) {
|
|
7754
8325
|
throw new Error(`Unknown category "${opts.category}".`);
|
|
@@ -7767,7 +8338,7 @@ async function runRecommend(need, opts) {
|
|
|
7767
8338
|
}
|
|
7768
8339
|
console.log(
|
|
7769
8340
|
table(
|
|
7770
|
-
["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category"],
|
|
8341
|
+
["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category", "Flags"],
|
|
7771
8342
|
candidates.map((c) => [
|
|
7772
8343
|
c.name,
|
|
7773
8344
|
String(c.healthScore),
|
|
@@ -7775,7 +8346,11 @@ async function runRecommend(need, opts) {
|
|
|
7775
8346
|
confidenceLabel(c.confidence),
|
|
7776
8347
|
formatNumber(c.weeklyDownloads),
|
|
7777
8348
|
c.latestVersion ?? "\u2014",
|
|
7778
|
-
c.category ?? "\u2014"
|
|
8349
|
+
c.category ?? "\u2014",
|
|
8350
|
+
// Last, and the only coloured cell: it is the column that can reverse
|
|
8351
|
+
// the decision the other seven are supporting, so it reads as a verdict
|
|
8352
|
+
// at the end of the row rather than as one more attribute among them.
|
|
8353
|
+
candidateFlags(c)
|
|
7779
8354
|
])
|
|
7780
8355
|
)
|
|
7781
8356
|
);
|
|
@@ -7873,10 +8448,31 @@ async function runCompare(pkgs, opts) {
|
|
|
7873
8448
|
])
|
|
7874
8449
|
)
|
|
7875
8450
|
);
|
|
7876
|
-
|
|
7877
|
-
|
|
8451
|
+
const notFound = res.notFound ?? (res.pending ? [] : res.missing ?? []);
|
|
8452
|
+
if (notFound.length) console.log(red(`
|
|
8453
|
+
not on npm: ${notFound.join(", ")}`));
|
|
8454
|
+
if (res.pending?.length) {
|
|
8455
|
+
console.log(dim(`
|
|
8456
|
+
being scored now (retry shortly): ${res.pending.join(", ")}`));
|
|
8457
|
+
}
|
|
7878
8458
|
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
7879
8459
|
}
|
|
8460
|
+
function runCan(query, opts = {}) {
|
|
8461
|
+
const matches = searchCapabilities(query, 5);
|
|
8462
|
+
if (opts.json) {
|
|
8463
|
+
console.log(JSON.stringify(matches, null, 2));
|
|
8464
|
+
return;
|
|
8465
|
+
}
|
|
8466
|
+
if (!query.trim()) console.log(dim("Showing everything lurq does. Add words to narrow it.\n"));
|
|
8467
|
+
for (const m of matches) {
|
|
8468
|
+
console.log(bold(m.title));
|
|
8469
|
+
console.log(` ${m.answer}`);
|
|
8470
|
+
if (m.cli) console.log(` ${green("$")} ${m.cli}`);
|
|
8471
|
+
if (m.mcp) console.log(dim(` agents: call the \`${m.mcp}\` tool`));
|
|
8472
|
+
if (m.page) console.log(dim(` dashboard: https://lurq.run${m.page}`));
|
|
8473
|
+
console.log("");
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
7880
8476
|
function runWeights(opts = {}) {
|
|
7881
8477
|
const w = loadWeights();
|
|
7882
8478
|
const active2 = activeWeightsPath();
|
|
@@ -7947,10 +8543,10 @@ async function runEditWeights(opts) {
|
|
|
7947
8543
|
runWeights(opts);
|
|
7948
8544
|
}
|
|
7949
8545
|
async function runPlan(file, opts) {
|
|
7950
|
-
const { readFileSync:
|
|
8546
|
+
const { readFileSync: readFileSync11 } = await import("fs");
|
|
7951
8547
|
let document;
|
|
7952
8548
|
try {
|
|
7953
|
-
document =
|
|
8549
|
+
document = readFileSync11(file, "utf8");
|
|
7954
8550
|
} catch {
|
|
7955
8551
|
throw new Error(`Could not read "${file}".`);
|
|
7956
8552
|
}
|
|
@@ -8115,6 +8711,7 @@ async function runUsage(pkg, opts) {
|
|
|
8115
8711
|
const reqs = Object.entries(res.engines).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
8116
8712
|
if (reqs) console.log(dim(`requires: ${reqs}`));
|
|
8117
8713
|
}
|
|
8714
|
+
if (res.unpublishedVersion) return console.log(red(res.note ?? "that version does not exist"));
|
|
8118
8715
|
if (!res.available) return console.log(dim(res.note ?? "no API surface available"));
|
|
8119
8716
|
console.log(
|
|
8120
8717
|
table(
|
|
@@ -8137,6 +8734,9 @@ async function runUsage(pkg, opts) {
|
|
|
8137
8734
|
if (!removed.length && !added.length && !renamed.length && !changed.length) {
|
|
8138
8735
|
console.log(dim(" no API changes"));
|
|
8139
8736
|
}
|
|
8737
|
+
} else if (res.deltaNote) {
|
|
8738
|
+
console.log(yellow(`
|
|
8739
|
+
${res.deltaNote}`));
|
|
8140
8740
|
}
|
|
8141
8741
|
}
|
|
8142
8742
|
async function runCompatBackfill(opts) {
|
|
@@ -8243,6 +8843,7 @@ var init_commands = __esm({
|
|
|
8243
8843
|
init_open();
|
|
8244
8844
|
init_userConfig();
|
|
8245
8845
|
init_types();
|
|
8846
|
+
init_capabilities();
|
|
8246
8847
|
init_client();
|
|
8247
8848
|
init_packages();
|
|
8248
8849
|
init_handlers();
|
|
@@ -8274,15 +8875,15 @@ function appJwt(creds, now = /* @__PURE__ */ new Date()) {
|
|
|
8274
8875
|
const signature = createSign("RSA-SHA256").update(`${header}.${payload}`).sign(creds.privateKey);
|
|
8275
8876
|
return `${header}.${payload}.${b64url(signature)}`;
|
|
8276
8877
|
}
|
|
8277
|
-
async function installationToken(
|
|
8278
|
-
const cached3 = tokenCache.get(
|
|
8878
|
+
async function installationToken(installationId2) {
|
|
8879
|
+
const cached3 = tokenCache.get(installationId2);
|
|
8279
8880
|
if (cached3 && cached3.expiresAt - TOKEN_SKEW_MS > Date.now()) return cached3.token;
|
|
8280
8881
|
const creds = githubAppCredentials();
|
|
8281
8882
|
if (!creds) throw new GithubAppError("GitHub App is not configured.", 503);
|
|
8282
8883
|
let res;
|
|
8283
8884
|
try {
|
|
8284
8885
|
res = await httpRequest(
|
|
8285
|
-
`${API}/app/installations/${
|
|
8886
|
+
`${API}/app/installations/${installationId2}/access_tokens`,
|
|
8286
8887
|
{
|
|
8287
8888
|
host: API_HOST,
|
|
8288
8889
|
ttlMs: 0,
|
|
@@ -8307,7 +8908,7 @@ async function installationToken(installationId) {
|
|
|
8307
8908
|
const token = res.data?.token;
|
|
8308
8909
|
if (!token) throw new GithubAppError("GitHub returned no installation token.", 502);
|
|
8309
8910
|
const expiresAt = res.data.expires_at ? Date.parse(res.data.expires_at) : NaN;
|
|
8310
|
-
tokenCache.set(
|
|
8911
|
+
tokenCache.set(installationId2, {
|
|
8311
8912
|
token,
|
|
8312
8913
|
// A token with an unparseable expiry is still valid; treat it as short-lived
|
|
8313
8914
|
// rather than caching it forever on a malformed timestamp.
|
|
@@ -8315,8 +8916,8 @@ async function installationToken(installationId) {
|
|
|
8315
8916
|
});
|
|
8316
8917
|
return token;
|
|
8317
8918
|
}
|
|
8318
|
-
async function installationGet(
|
|
8319
|
-
const token = await installationToken(
|
|
8919
|
+
async function installationGet(installationId2, path2) {
|
|
8920
|
+
const token = await installationToken(installationId2);
|
|
8320
8921
|
const res = await httpRequest(`${API}${path2}`, {
|
|
8321
8922
|
host: API_HOST,
|
|
8322
8923
|
ttlMs: 0,
|
|
@@ -8422,8 +9023,15 @@ jobs:
|
|
|
8422
9023
|
|
|
8423
9024
|
# 2. Narrow that to symbols THIS repo references. Runs entirely locally
|
|
8424
9025
|
# against both versions' npm tarballs, no API key, no test suite.
|
|
9026
|
+
#
|
|
9027
|
+
# \`--report\` is the only part that talks to us, and it is additive: it
|
|
9028
|
+
# posts what this check concluded so your dashboard can show it. A
|
|
9029
|
+
# missing key or an unreachable API prints a line and changes nothing
|
|
9030
|
+
# else, so the gate keeps working exactly as it does without it.
|
|
8425
9031
|
- name: Check against this codebase
|
|
8426
|
-
|
|
9032
|
+
env:
|
|
9033
|
+
LURQ_API_KEY: \${{ secrets.LURQ_API_KEY }}
|
|
9034
|
+
run: npx -y ${cli} check-upgrade . --plan lurq-plan.json --report --json > lurq-brief.json
|
|
8427
9035
|
|
|
8428
9036
|
# The same report twice: once into the run summary, once as the body of
|
|
8429
9037
|
# the pull request. Fenced, because the report is aligned plain text and
|
|
@@ -8547,22 +9155,23 @@ var init_workflow = __esm({
|
|
|
8547
9155
|
});
|
|
8548
9156
|
|
|
8549
9157
|
// src/github/manifests.ts
|
|
8550
|
-
async function listInstallationRepos(
|
|
9158
|
+
async function listInstallationRepos(installationId2) {
|
|
8551
9159
|
const out = [];
|
|
8552
9160
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
8553
9161
|
const data = await installationGet(
|
|
8554
|
-
|
|
8555
|
-
`/installation/repositories?per_page=${
|
|
9162
|
+
installationId2,
|
|
9163
|
+
`/installation/repositories?per_page=${PAGE_SIZE2}&page=${page}`
|
|
8556
9164
|
);
|
|
8557
9165
|
const batch = data.repositories ?? [];
|
|
8558
9166
|
for (const repo of batch) {
|
|
8559
9167
|
out.push({
|
|
8560
9168
|
fullName: repo.full_name,
|
|
8561
9169
|
defaultBranch: repo.default_branch,
|
|
8562
|
-
isPrivate: repo.private
|
|
9170
|
+
isPrivate: repo.private,
|
|
9171
|
+
pushedAt: repo.pushed_at ?? null
|
|
8563
9172
|
});
|
|
8564
9173
|
}
|
|
8565
|
-
if (batch.length <
|
|
9174
|
+
if (batch.length < PAGE_SIZE2) break;
|
|
8566
9175
|
}
|
|
8567
9176
|
return out;
|
|
8568
9177
|
}
|
|
@@ -8589,9 +9198,9 @@ function parseManifest(path2, json2) {
|
|
|
8589
9198
|
}
|
|
8590
9199
|
return Object.keys(deps).length > 0 ? { path: path2, deps } : null;
|
|
8591
9200
|
}
|
|
8592
|
-
async function fetchManifests(
|
|
9201
|
+
async function fetchManifests(installationId2, fullName, branch) {
|
|
8593
9202
|
const tree = await installationGet(
|
|
8594
|
-
|
|
9203
|
+
installationId2,
|
|
8595
9204
|
`/repos/${fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`
|
|
8596
9205
|
);
|
|
8597
9206
|
const nodes = tree.tree ?? [];
|
|
@@ -8602,7 +9211,7 @@ async function fetchManifests(installationId, fullName, branch) {
|
|
|
8602
9211
|
for (const path2 of paths) {
|
|
8603
9212
|
try {
|
|
8604
9213
|
const res = await installationGet(
|
|
8605
|
-
|
|
9214
|
+
installationId2,
|
|
8606
9215
|
`/repos/${fullName}/contents/${path2.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(branch)}`
|
|
8607
9216
|
);
|
|
8608
9217
|
if (!res.content || res.encoding !== "base64") continue;
|
|
@@ -8621,7 +9230,7 @@ async function fetchManifests(installationId, fullName, branch) {
|
|
|
8621
9230
|
partial: Boolean(tree.truncated) || allPaths.length > paths.length
|
|
8622
9231
|
};
|
|
8623
9232
|
}
|
|
8624
|
-
var
|
|
9233
|
+
var PAGE_SIZE2, MAX_PAGES;
|
|
8625
9234
|
var init_manifests = __esm({
|
|
8626
9235
|
"src/github/manifests.ts"() {
|
|
8627
9236
|
"use strict";
|
|
@@ -8630,7 +9239,7 @@ var init_manifests = __esm({
|
|
|
8630
9239
|
init_workflow();
|
|
8631
9240
|
init_types2();
|
|
8632
9241
|
init_logger();
|
|
8633
|
-
|
|
9242
|
+
PAGE_SIZE2 = 100;
|
|
8634
9243
|
MAX_PAGES = 20;
|
|
8635
9244
|
}
|
|
8636
9245
|
});
|
|
@@ -8728,8 +9337,8 @@ function formatUpgradePlan(plan) {
|
|
|
8728
9337
|
if (upgrade.removed.length) {
|
|
8729
9338
|
out.push(` removes ${upgrade.removed.length}: ${upgrade.removed.slice(0, 8).join(", ")}`);
|
|
8730
9339
|
}
|
|
8731
|
-
for (const
|
|
8732
|
-
out.push(` arity ${
|
|
9340
|
+
for (const change2 of upgrade.arityChanged.slice(0, 5)) {
|
|
9341
|
+
out.push(` arity ${change2.path}: ${change2.from ?? "?"} \u2192 ${change2.to ?? "?"}`);
|
|
8733
9342
|
}
|
|
8734
9343
|
if (upgrade.hops?.length) {
|
|
8735
9344
|
out.push(` via ${[upgrade.fromVersion, ...upgrade.hops.map((h) => h.toVersion)].join(" \u2192 ")}`);
|
|
@@ -8770,6 +9379,68 @@ var init_upgradePlan = __esm({
|
|
|
8770
9379
|
}
|
|
8771
9380
|
});
|
|
8772
9381
|
|
|
9382
|
+
// src/cli/reportRuns.ts
|
|
9383
|
+
var reportRuns_exports = {};
|
|
9384
|
+
__export(reportRuns_exports, {
|
|
9385
|
+
buildRunReports: () => buildRunReports,
|
|
9386
|
+
runContextFromEnv: () => runContextFromEnv
|
|
9387
|
+
});
|
|
9388
|
+
function findingRefs(finding) {
|
|
9389
|
+
return [
|
|
9390
|
+
...finding.symbolsRemoved.flatMap((s) => s.refs),
|
|
9391
|
+
...finding.arityChanged.flatMap((a) => a.refs)
|
|
9392
|
+
];
|
|
9393
|
+
}
|
|
9394
|
+
function buildRunReports(report, targets, ctx) {
|
|
9395
|
+
const versions = new Map(targets.map((t) => [t.package, t]));
|
|
9396
|
+
const base = (pkg) => {
|
|
9397
|
+
const target = versions.get(pkg);
|
|
9398
|
+
return {
|
|
9399
|
+
repoFullName: ctx.repoFullName,
|
|
9400
|
+
packageName: pkg,
|
|
9401
|
+
fromVersion: target?.fromVersion ?? "",
|
|
9402
|
+
toVersion: target?.toVersion ?? "",
|
|
9403
|
+
status: "checked",
|
|
9404
|
+
runUrl: ctx.runUrl
|
|
9405
|
+
};
|
|
9406
|
+
};
|
|
9407
|
+
const rows = [];
|
|
9408
|
+
for (const finding of report.breaking) {
|
|
9409
|
+
const refs = findingRefs(finding);
|
|
9410
|
+
rows.push({
|
|
9411
|
+
...base(finding.package),
|
|
9412
|
+
// The finding carries its own versions; prefer them over the target map.
|
|
9413
|
+
fromVersion: finding.fromVersion,
|
|
9414
|
+
toVersion: finding.toVersion,
|
|
9415
|
+
severity: finding.severity,
|
|
9416
|
+
symbolsAffected: [
|
|
9417
|
+
...finding.symbolsRemoved.map((s) => s.symbol),
|
|
9418
|
+
...finding.arityChanged.map((a) => a.symbol)
|
|
9419
|
+
],
|
|
9420
|
+
callSites: refs.length,
|
|
9421
|
+
callSiteFiles: [...new Set(refs.map((r) => r.file))]
|
|
9422
|
+
});
|
|
9423
|
+
}
|
|
9424
|
+
for (const pkg of report.ok) rows.push({ ...base(pkg), severity: "ok" });
|
|
9425
|
+
for (const u of report.unverified) rows.push({ ...base(u.package), severity: "unverified" });
|
|
9426
|
+
return rows.filter((r) => r.fromVersion && r.toVersion).slice(0, MAX_RUNS);
|
|
9427
|
+
}
|
|
9428
|
+
function runContextFromEnv(env = process.env) {
|
|
9429
|
+
const repoFullName = env.GITHUB_REPOSITORY?.trim();
|
|
9430
|
+
if (!repoFullName) return null;
|
|
9431
|
+
const server = env.GITHUB_SERVER_URL?.trim() || "https://github.com";
|
|
9432
|
+
const runId = env.GITHUB_RUN_ID?.trim();
|
|
9433
|
+
return { repoFullName, runUrl: runId ? `${server}/${repoFullName}/actions/runs/${runId}` : "" };
|
|
9434
|
+
}
|
|
9435
|
+
var MAX_RUNS;
|
|
9436
|
+
var init_reportRuns = __esm({
|
|
9437
|
+
"src/cli/reportRuns.ts"() {
|
|
9438
|
+
"use strict";
|
|
9439
|
+
init_esm_shims();
|
|
9440
|
+
MAX_RUNS = 100;
|
|
9441
|
+
}
|
|
9442
|
+
});
|
|
9443
|
+
|
|
8773
9444
|
// src/surface/references.ts
|
|
8774
9445
|
var references_exports = {};
|
|
8775
9446
|
__export(references_exports, {
|
|
@@ -8861,10 +9532,10 @@ function scanReferences(rootDir, opts = {}) {
|
|
|
8861
9532
|
const record = (pkg, symbol, via, specifier, file, line, parent) => {
|
|
8862
9533
|
let syms = byPackage.get(pkg);
|
|
8863
9534
|
if (!syms) byPackage.set(pkg, syms = /* @__PURE__ */ new Map());
|
|
8864
|
-
const
|
|
9535
|
+
const list2 = syms.get(symbol);
|
|
8865
9536
|
const ref = { symbol, via, specifier, file, line, ...parent ? { parent } : {} };
|
|
8866
|
-
if (
|
|
8867
|
-
if (!
|
|
9537
|
+
if (list2) {
|
|
9538
|
+
if (!list2.some((r) => r.file === file && r.line === line && r.via === via)) list2.push(ref);
|
|
8868
9539
|
} else {
|
|
8869
9540
|
syms.set(symbol, [ref]);
|
|
8870
9541
|
}
|
|
@@ -9650,8 +10321,8 @@ function groupByEntry(refs, pkg) {
|
|
|
9650
10321
|
const sub = use.specifier === pkg ? "" : use.specifier.slice(pkg.length + 1);
|
|
9651
10322
|
let symbols2 = byEntry.get(sub);
|
|
9652
10323
|
if (!symbols2) byEntry.set(sub, symbols2 = /* @__PURE__ */ new Map());
|
|
9653
|
-
const
|
|
9654
|
-
if (
|
|
10324
|
+
const list2 = symbols2.get(sym);
|
|
10325
|
+
if (list2) list2.push(use);
|
|
9655
10326
|
else symbols2.set(sym, [use]);
|
|
9656
10327
|
}
|
|
9657
10328
|
}
|
|
@@ -9872,6 +10543,32 @@ function targetsFromPlanFile(path2) {
|
|
|
9872
10543
|
}
|
|
9873
10544
|
return parsed.upgrades.filter((u) => u.package && u.fromVersion && u.toVersion).filter((u) => u.inScope !== false).map((u) => ({ package: u.package, fromVersion: u.fromVersion, toVersion: u.toVersion }));
|
|
9874
10545
|
}
|
|
10546
|
+
async function reportOutcome(report, targets, opts) {
|
|
10547
|
+
const { buildRunReports: buildRunReports2, runContextFromEnv: runContextFromEnv2 } = await Promise.resolve().then(() => (init_reportRuns(), reportRuns_exports));
|
|
10548
|
+
const ctx = runContextFromEnv2();
|
|
10549
|
+
if (!ctx) {
|
|
10550
|
+
console.error("--report: no $GITHUB_REPOSITORY, so there is no run to attribute this to. Skipped.");
|
|
10551
|
+
return;
|
|
10552
|
+
}
|
|
10553
|
+
const runs = buildRunReports2(report, targets, ctx);
|
|
10554
|
+
if (runs.length === 0) return;
|
|
10555
|
+
try {
|
|
10556
|
+
const { reportUpgradeRuns: reportUpgradeRuns2 } = await Promise.resolve().then(() => (init_remote(), remote_exports));
|
|
10557
|
+
const { recorded, rejected } = await reportUpgradeRuns2(runs, {
|
|
10558
|
+
url: opts.url,
|
|
10559
|
+
apiKey: opts.apiKey
|
|
10560
|
+
});
|
|
10561
|
+
console.error(
|
|
10562
|
+
`reported ${recorded} upgrade result(s) to the lurq dashboard` + (rejected ? ` (${rejected} rejected)` : "")
|
|
10563
|
+
);
|
|
10564
|
+
} catch (err) {
|
|
10565
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10566
|
+
const noKey = /no api key/i.test(message);
|
|
10567
|
+
console.error(
|
|
10568
|
+
noKey ? "--report: no API key, so there was nothing to report with. Set LURQ_API_KEY in this repository's secrets to see these results on your dashboard. The check itself is unaffected." : `--report: could not reach the lurq dashboard (${message}). The check itself is unaffected.`
|
|
10569
|
+
);
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
9875
10572
|
async function runCheckUpgrade(dir, opts) {
|
|
9876
10573
|
const { scanReferences: scanReferences2 } = await Promise.resolve().then(() => (init_references(), references_exports));
|
|
9877
10574
|
const { checkUpgrade: checkUpgrade2, formatUpgradeReport: formatUpgradeReport2 } = await Promise.resolve().then(() => (init_upgrade(), upgrade_exports));
|
|
@@ -9892,6 +10589,7 @@ async function runCheckUpgrade(dir, opts) {
|
|
|
9892
10589
|
console.log(
|
|
9893
10590
|
opts.json ? JSON.stringify(report, null, 2) : formatUpgradeReport2(report, `upgrade check on ${dir}`)
|
|
9894
10591
|
);
|
|
10592
|
+
if (opts.report) await reportOutcome(report, targets, opts);
|
|
9895
10593
|
if (opts.exitCode && !report.safe) process.exitCode = 1;
|
|
9896
10594
|
}
|
|
9897
10595
|
var init_checkUpgrade = __esm({
|
|
@@ -9901,6 +10599,459 @@ var init_checkUpgrade = __esm({
|
|
|
9901
10599
|
}
|
|
9902
10600
|
});
|
|
9903
10601
|
|
|
10602
|
+
// src/surface/release.ts
|
|
10603
|
+
var release_exports = {};
|
|
10604
|
+
__export(release_exports, {
|
|
10605
|
+
checkRelease: () => checkRelease,
|
|
10606
|
+
formatReleaseCheck: () => formatReleaseCheck,
|
|
10607
|
+
releaseVerdict: () => releaseVerdict
|
|
10608
|
+
});
|
|
10609
|
+
import semver5 from "semver";
|
|
10610
|
+
function declaredBump(from, to) {
|
|
10611
|
+
if (!semver5.gt(to, from)) return null;
|
|
10612
|
+
if (semver5.major(to) !== semver5.major(from)) return "major";
|
|
10613
|
+
if (semver5.minor(to) !== semver5.minor(from)) return "minor";
|
|
10614
|
+
return "patch";
|
|
10615
|
+
}
|
|
10616
|
+
function covers(declared, required, baselineMajor) {
|
|
10617
|
+
if (baselineMajor === 0 && required === "major") return RANK[declared] >= RANK.minor;
|
|
10618
|
+
return RANK[declared] >= RANK[required];
|
|
10619
|
+
}
|
|
10620
|
+
function releaseVerdict(publishedVersion, localVersion, diff) {
|
|
10621
|
+
const required = diff.removed.length || diff.arityChanged.length || diff.typeOnlyRemoved.length ? "major" : diff.added.length ? "minor" : "patch";
|
|
10622
|
+
const declared = declaredBump(publishedVersion, localVersion);
|
|
10623
|
+
const verdict = declared !== null && covers(declared, required, semver5.major(publishedVersion)) ? "ok" : "understated";
|
|
10624
|
+
return { declared, required, verdict };
|
|
10625
|
+
}
|
|
10626
|
+
async function checkRelease(dir, opts = {}) {
|
|
10627
|
+
const manifest = readManifest(dir);
|
|
10628
|
+
const name = manifest?.name;
|
|
10629
|
+
const base = {
|
|
10630
|
+
package: name ?? dir,
|
|
10631
|
+
publishedVersion: null,
|
|
10632
|
+
localVersion: manifest?.version ?? null
|
|
10633
|
+
};
|
|
10634
|
+
if (!name) return inconclusive(base, `no readable package.json in ${dir}`);
|
|
10635
|
+
if (!manifest.version) return inconclusive(base, "package.json has no version field");
|
|
10636
|
+
const { extractSurface: extractSurface3 } = await Promise.resolve().then(() => (init_extract2(), extract_exports));
|
|
10637
|
+
const local = extractSurface3(dir, { manifest });
|
|
10638
|
+
if (local.undeclaredReason) {
|
|
10639
|
+
return inconclusive(base, `${local.undeclaredReason} \u2014 build the package first`);
|
|
10640
|
+
}
|
|
10641
|
+
const published = await fetchAndExtract(name, opts.against ?? "latest", {
|
|
10642
|
+
fetchImpl: opts.fetchImpl
|
|
10643
|
+
});
|
|
10644
|
+
if (!published) {
|
|
10645
|
+
return inconclusive(base, `${name} has no published version to compare against`);
|
|
10646
|
+
}
|
|
10647
|
+
const withVersion = { ...base, publishedVersion: published.resolvedVersion };
|
|
10648
|
+
const diff = diffSurfaces(published.surface, local);
|
|
10649
|
+
if (diff.inconclusive) return inconclusive(withVersion, diff.inconclusive);
|
|
10650
|
+
return {
|
|
10651
|
+
...withVersion,
|
|
10652
|
+
...releaseVerdict(published.resolvedVersion, manifest.version, diff),
|
|
10653
|
+
removed: diff.removed.map((s) => s.path),
|
|
10654
|
+
arityChanged: diff.arityChanged,
|
|
10655
|
+
typeOnlyRemoved: diff.typeOnlyRemoved.map((s) => s.path),
|
|
10656
|
+
added: diff.added.map((s) => s.path)
|
|
10657
|
+
};
|
|
10658
|
+
}
|
|
10659
|
+
function formatReleaseCheck(c) {
|
|
10660
|
+
const out = [`lurq, release check \u2014 ${c.package}`, ""];
|
|
10661
|
+
if (c.verdict === "inconclusive") {
|
|
10662
|
+
out.push(`INCONCLUSIVE ${c.inconclusive}`, "", "No verdict. This is not a pass.");
|
|
10663
|
+
return out.join("\n");
|
|
10664
|
+
}
|
|
10665
|
+
const from = c.publishedVersion ?? "?";
|
|
10666
|
+
const to = c.localVersion ?? "?";
|
|
10667
|
+
out.push(
|
|
10668
|
+
c.verdict === "ok" ? c.declared === c.required ? `OK ${from} \u2192 ${to} is a ${c.declared}, which is what this API change needs.` : `OK ${from} \u2192 ${to} is a ${c.declared}; this API change only needed a ${c.required}.` : c.declared === null ? `UNDERSTATED ${to} is not ahead of the published ${from}.` : `UNDERSTATED ${from} \u2192 ${to} is tagged ${c.declared}, but this API change is a ${c.required}.`,
|
|
10669
|
+
""
|
|
10670
|
+
);
|
|
10671
|
+
out.push(...list("Removed exports", c.removed));
|
|
10672
|
+
if (c.arityChanged.length) {
|
|
10673
|
+
out.push(` Parameter count changed (${c.arityChanged.length}):`);
|
|
10674
|
+
for (const a of c.arityChanged.slice(0, LIST_CAP)) {
|
|
10675
|
+
out.push(` \xB7 ${a.path} ${a.from} \u2192 ${a.to}`);
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
out.push(...list("Removed types (breaks tsc, not node)", c.typeOnlyRemoved));
|
|
10679
|
+
out.push(...list("Added exports", c.added));
|
|
10680
|
+
if (!c.removed.length && !c.arityChanged.length && !c.typeOnlyRemoved.length && !c.added.length) {
|
|
10681
|
+
out.push(" No change to the exported surface.");
|
|
10682
|
+
}
|
|
10683
|
+
return out.join("\n");
|
|
10684
|
+
}
|
|
10685
|
+
var RANK, inconclusive, LIST_CAP, list;
|
|
10686
|
+
var init_release = __esm({
|
|
10687
|
+
"src/surface/release.ts"() {
|
|
10688
|
+
"use strict";
|
|
10689
|
+
init_esm_shims();
|
|
10690
|
+
init_diff2();
|
|
10691
|
+
init_fetch();
|
|
10692
|
+
init_resolve();
|
|
10693
|
+
RANK = { patch: 0, minor: 1, major: 2 };
|
|
10694
|
+
inconclusive = (base, reason) => ({
|
|
10695
|
+
...base,
|
|
10696
|
+
declared: null,
|
|
10697
|
+
required: "patch",
|
|
10698
|
+
verdict: "inconclusive",
|
|
10699
|
+
removed: [],
|
|
10700
|
+
arityChanged: [],
|
|
10701
|
+
typeOnlyRemoved: [],
|
|
10702
|
+
added: [],
|
|
10703
|
+
inconclusive: reason
|
|
10704
|
+
});
|
|
10705
|
+
LIST_CAP = 12;
|
|
10706
|
+
list = (label, items) => {
|
|
10707
|
+
if (!items.length) return [];
|
|
10708
|
+
const shown = items.slice(0, LIST_CAP);
|
|
10709
|
+
const more = items.length - shown.length;
|
|
10710
|
+
return [
|
|
10711
|
+
` ${label} (${items.length}):`,
|
|
10712
|
+
...shown.map((s) => ` \xB7 ${s}`),
|
|
10713
|
+
...more > 0 ? [` \u2026 ${more} more`] : []
|
|
10714
|
+
];
|
|
10715
|
+
};
|
|
10716
|
+
}
|
|
10717
|
+
});
|
|
10718
|
+
|
|
10719
|
+
// src/api/diff.ts
|
|
10720
|
+
var diff_exports = {};
|
|
10721
|
+
__export(diff_exports, {
|
|
10722
|
+
checkApi: () => checkApi,
|
|
10723
|
+
diffApiSurfaces: () => diffApiSurfaces,
|
|
10724
|
+
formatApiCheck: () => formatApiCheck
|
|
10725
|
+
});
|
|
10726
|
+
import semver6 from "semver";
|
|
10727
|
+
function compareOperation(before, after) {
|
|
10728
|
+
const out = [];
|
|
10729
|
+
for (const param of missing(after.requiredParams, before.requiredParams)) {
|
|
10730
|
+
out.push(
|
|
10731
|
+
change(
|
|
10732
|
+
before.optionalParams.includes(param) ? "param-now-required" : "required-param-added",
|
|
10733
|
+
after.id,
|
|
10734
|
+
param
|
|
10735
|
+
)
|
|
10736
|
+
);
|
|
10737
|
+
}
|
|
10738
|
+
for (const param of missing(after.optionalParams, before.optionalParams)) {
|
|
10739
|
+
out.push(change("optional-param-added", after.id, param));
|
|
10740
|
+
}
|
|
10741
|
+
for (const field of missing(after.requiredBodyFields, before.requiredBodyFields)) {
|
|
10742
|
+
out.push(change("required-body-field-added", after.id, field));
|
|
10743
|
+
}
|
|
10744
|
+
for (const code of missing(before.responseCodes, after.responseCodes)) {
|
|
10745
|
+
out.push(change("response-removed", after.id, code));
|
|
10746
|
+
}
|
|
10747
|
+
for (const code of missing(after.responseCodes, before.responseCodes)) {
|
|
10748
|
+
out.push(change("response-added", after.id, code));
|
|
10749
|
+
}
|
|
10750
|
+
if (after.deprecated && !before.deprecated) out.push(change("operation-deprecated", after.id));
|
|
10751
|
+
return out;
|
|
10752
|
+
}
|
|
10753
|
+
function diffApiSurfaces(before, after) {
|
|
10754
|
+
if (before.unreadableReason) return { breaking: [], other: [], inconclusive: `previous revision: ${before.unreadableReason}` };
|
|
10755
|
+
if (after.unreadableReason) return { breaking: [], other: [], inconclusive: `current revision: ${after.unreadableReason}` };
|
|
10756
|
+
const changes = [];
|
|
10757
|
+
for (const [id, op] of before.operations) {
|
|
10758
|
+
const next = after.operations.get(id);
|
|
10759
|
+
if (!next) {
|
|
10760
|
+
changes.push(change("operation-removed", id));
|
|
10761
|
+
continue;
|
|
10762
|
+
}
|
|
10763
|
+
changes.push(...compareOperation(op, next));
|
|
10764
|
+
}
|
|
10765
|
+
for (const id of after.operations.keys()) {
|
|
10766
|
+
if (!before.operations.has(id)) changes.push(change("operation-added", id));
|
|
10767
|
+
}
|
|
10768
|
+
return {
|
|
10769
|
+
breaking: changes.filter((c) => c.breaking),
|
|
10770
|
+
other: changes.filter((c) => !c.breaking)
|
|
10771
|
+
};
|
|
10772
|
+
}
|
|
10773
|
+
function checkApi(before, after) {
|
|
10774
|
+
const diff = diffApiSurfaces(before, after);
|
|
10775
|
+
const base = {
|
|
10776
|
+
title: after.title ?? before.title,
|
|
10777
|
+
fromVersion: before.version,
|
|
10778
|
+
toVersion: after.version,
|
|
10779
|
+
diff
|
|
10780
|
+
};
|
|
10781
|
+
if (diff.inconclusive) {
|
|
10782
|
+
return { ...base, verdict: "inconclusive", versionCovers: null };
|
|
10783
|
+
}
|
|
10784
|
+
const from = before.version && semver6.valid(before.version);
|
|
10785
|
+
const to = after.version && semver6.valid(after.version);
|
|
10786
|
+
const versionCovers = from && to ? diff.breaking.length > 0 ? semver6.major(to) > semver6.major(from) : semver6.gte(to, from) : null;
|
|
10787
|
+
return { ...base, verdict: diff.breaking.length > 0 ? "breaking" : "ok", versionCovers };
|
|
10788
|
+
}
|
|
10789
|
+
function renderChanges(changes) {
|
|
10790
|
+
const shown = changes.slice(0, LIST_CAP2);
|
|
10791
|
+
const lines = shown.map(
|
|
10792
|
+
(c) => ` \xB7 ${c.operation}${c.detail ? ` \u2192 ${c.detail}` : ""} ${LABEL[c.kind]}`
|
|
10793
|
+
);
|
|
10794
|
+
if (changes.length > shown.length) lines.push(` \u2026 ${changes.length - shown.length} more`);
|
|
10795
|
+
return lines;
|
|
10796
|
+
}
|
|
10797
|
+
function formatApiCheck(check2, label = "api check") {
|
|
10798
|
+
const out = [`lurq, ${label}${check2.title ? ` \u2014 ${check2.title}` : ""}`, ""];
|
|
10799
|
+
if (check2.verdict === "inconclusive") {
|
|
10800
|
+
out.push(`INCONCLUSIVE ${check2.diff.inconclusive}`, "", "No verdict. This is not a pass.");
|
|
10801
|
+
return out.join("\n");
|
|
10802
|
+
}
|
|
10803
|
+
const versions = check2.fromVersion && check2.toVersion ? ` ${check2.fromVersion} \u2192 ${check2.toVersion}` : "";
|
|
10804
|
+
if (check2.diff.breaking.length) {
|
|
10805
|
+
out.push(`BREAKING${versions}`, "");
|
|
10806
|
+
out.push(` ${check2.diff.breaking.length} change(s) that break existing callers:`);
|
|
10807
|
+
out.push(...renderChanges(check2.diff.breaking));
|
|
10808
|
+
} else {
|
|
10809
|
+
out.push(`OK${versions}`, "", " Nothing here breaks an existing caller.");
|
|
10810
|
+
}
|
|
10811
|
+
if (check2.diff.other.length) {
|
|
10812
|
+
out.push("", ` ${check2.diff.other.length} other change(s):`);
|
|
10813
|
+
out.push(...renderChanges(check2.diff.other));
|
|
10814
|
+
}
|
|
10815
|
+
if (check2.versionCovers === false) {
|
|
10816
|
+
out.push(
|
|
10817
|
+
"",
|
|
10818
|
+
check2.diff.breaking.length ? ` info.version went ${check2.fromVersion} \u2192 ${check2.toVersion}. A breaking change needs a major.` : ` info.version went ${check2.fromVersion} \u2192 ${check2.toVersion}, which is not ahead.`
|
|
10819
|
+
);
|
|
10820
|
+
} else if (check2.versionCovers === null && (check2.fromVersion || check2.toVersion)) {
|
|
10821
|
+
out.push("", " info.version is not semver on both sides, so the bump was not judged.");
|
|
10822
|
+
}
|
|
10823
|
+
return out.join("\n");
|
|
10824
|
+
}
|
|
10825
|
+
var BREAKING, change, missing, LIST_CAP2, LABEL;
|
|
10826
|
+
var init_diff3 = __esm({
|
|
10827
|
+
"src/api/diff.ts"() {
|
|
10828
|
+
"use strict";
|
|
10829
|
+
init_esm_shims();
|
|
10830
|
+
BREAKING = /* @__PURE__ */ new Set([
|
|
10831
|
+
"operation-removed",
|
|
10832
|
+
"required-param-added",
|
|
10833
|
+
"param-now-required",
|
|
10834
|
+
"required-body-field-added",
|
|
10835
|
+
"response-removed"
|
|
10836
|
+
]);
|
|
10837
|
+
change = (kind, operation, detail2) => ({
|
|
10838
|
+
kind,
|
|
10839
|
+
operation,
|
|
10840
|
+
...detail2 ? { detail: detail2 } : {},
|
|
10841
|
+
breaking: BREAKING.has(kind)
|
|
10842
|
+
});
|
|
10843
|
+
missing = (a, b) => {
|
|
10844
|
+
const has = new Set(b);
|
|
10845
|
+
return a.filter((x) => !has.has(x));
|
|
10846
|
+
};
|
|
10847
|
+
LIST_CAP2 = 15;
|
|
10848
|
+
LABEL = {
|
|
10849
|
+
"operation-removed": "removed",
|
|
10850
|
+
"required-param-added": "new required param",
|
|
10851
|
+
"param-now-required": "now required",
|
|
10852
|
+
"required-body-field-added": "new required body field",
|
|
10853
|
+
"response-removed": "response removed",
|
|
10854
|
+
"operation-added": "added",
|
|
10855
|
+
"optional-param-added": "new optional param",
|
|
10856
|
+
"response-added": "response added",
|
|
10857
|
+
"operation-deprecated": "deprecated"
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
});
|
|
10861
|
+
|
|
10862
|
+
// src/api/openapi.ts
|
|
10863
|
+
import { parse as parseYaml } from "yaml";
|
|
10864
|
+
function deref(root, node, seen = /* @__PURE__ */ new Set()) {
|
|
10865
|
+
if (!isRecord(node)) return null;
|
|
10866
|
+
const ref = node.$ref;
|
|
10867
|
+
if (typeof ref !== "string") return node;
|
|
10868
|
+
if (!ref.startsWith("#/") || seen.has(ref)) return null;
|
|
10869
|
+
seen.add(ref);
|
|
10870
|
+
let cursor = root;
|
|
10871
|
+
for (const segment of ref.slice(2).split("/")) {
|
|
10872
|
+
if (!isRecord(cursor)) return null;
|
|
10873
|
+
cursor = cursor[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
10874
|
+
}
|
|
10875
|
+
return deref(root, cursor, seen);
|
|
10876
|
+
}
|
|
10877
|
+
function readParams(root, shared, own) {
|
|
10878
|
+
const required = /* @__PURE__ */ new Set();
|
|
10879
|
+
const optional = /* @__PURE__ */ new Set();
|
|
10880
|
+
for (const list2 of [shared, own]) {
|
|
10881
|
+
if (!Array.isArray(list2)) continue;
|
|
10882
|
+
for (const raw of list2) {
|
|
10883
|
+
const param = deref(root, raw);
|
|
10884
|
+
const name = param?.name;
|
|
10885
|
+
if (typeof name !== "string") continue;
|
|
10886
|
+
const key = `${name} (${typeof param?.in === "string" ? param.in : "unknown"})`;
|
|
10887
|
+
if (param?.required === true) {
|
|
10888
|
+
required.add(key);
|
|
10889
|
+
optional.delete(key);
|
|
10890
|
+
} else if (!required.has(key)) {
|
|
10891
|
+
optional.add(key);
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10894
|
+
}
|
|
10895
|
+
return { required: [...required].sort(), optional: [...optional].sort() };
|
|
10896
|
+
}
|
|
10897
|
+
function readBodyFields(root, requestBody) {
|
|
10898
|
+
const body = deref(root, requestBody);
|
|
10899
|
+
if (!body || body.required !== true) return [];
|
|
10900
|
+
const content = body.content;
|
|
10901
|
+
if (!isRecord(content)) return [];
|
|
10902
|
+
const out = /* @__PURE__ */ new Set();
|
|
10903
|
+
for (const media of Object.values(content)) {
|
|
10904
|
+
const schema = deref(root, isRecord(media) ? media.schema : null);
|
|
10905
|
+
const required = schema?.required;
|
|
10906
|
+
if (!Array.isArray(required)) continue;
|
|
10907
|
+
for (const field of required) if (typeof field === "string") out.add(field);
|
|
10908
|
+
}
|
|
10909
|
+
return [...out].sort();
|
|
10910
|
+
}
|
|
10911
|
+
function extractApiSurface(source, label = "document") {
|
|
10912
|
+
const empty2 = { title: null, version: null, operations: /* @__PURE__ */ new Map() };
|
|
10913
|
+
let doc;
|
|
10914
|
+
try {
|
|
10915
|
+
doc = parseYaml(source);
|
|
10916
|
+
} catch (err) {
|
|
10917
|
+
return {
|
|
10918
|
+
...empty2,
|
|
10919
|
+
unreadableReason: `${label} is not valid YAML or JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
10920
|
+
};
|
|
10921
|
+
}
|
|
10922
|
+
if (!isRecord(doc)) return { ...empty2, unreadableReason: `${label} is not an object` };
|
|
10923
|
+
if (!doc.openapi && !doc.swagger) {
|
|
10924
|
+
return { ...empty2, unreadableReason: `${label} has no openapi/swagger version field` };
|
|
10925
|
+
}
|
|
10926
|
+
const info = isRecord(doc.info) ? doc.info : {};
|
|
10927
|
+
const paths = isRecord(doc.paths) ? doc.paths : {};
|
|
10928
|
+
const operations = /* @__PURE__ */ new Map();
|
|
10929
|
+
for (const [path2, rawItem] of Object.entries(paths)) {
|
|
10930
|
+
const item = deref(doc, rawItem);
|
|
10931
|
+
if (!item) continue;
|
|
10932
|
+
for (const method of METHODS) {
|
|
10933
|
+
const op = item[method];
|
|
10934
|
+
if (!isRecord(op)) continue;
|
|
10935
|
+
const { required, optional } = readParams(doc, item.parameters, op.parameters);
|
|
10936
|
+
const responses = isRecord(op.responses) ? Object.keys(op.responses).sort() : [];
|
|
10937
|
+
const id = `${method.toUpperCase()} ${path2}`;
|
|
10938
|
+
operations.set(id, {
|
|
10939
|
+
id,
|
|
10940
|
+
method: method.toUpperCase(),
|
|
10941
|
+
path: path2,
|
|
10942
|
+
requiredParams: required,
|
|
10943
|
+
optionalParams: optional,
|
|
10944
|
+
requiredBodyFields: readBodyFields(doc, op.requestBody),
|
|
10945
|
+
responseCodes: responses,
|
|
10946
|
+
deprecated: op.deprecated === true
|
|
10947
|
+
});
|
|
10948
|
+
}
|
|
10949
|
+
}
|
|
10950
|
+
return {
|
|
10951
|
+
title: typeof info.title === "string" ? info.title : null,
|
|
10952
|
+
version: typeof info.version === "string" ? info.version : null,
|
|
10953
|
+
operations,
|
|
10954
|
+
...operations.size === 0 ? { unreadableReason: `${label} declares no operations` } : {}
|
|
10955
|
+
};
|
|
10956
|
+
}
|
|
10957
|
+
var METHODS, isRecord;
|
|
10958
|
+
var init_openapi = __esm({
|
|
10959
|
+
"src/api/openapi.ts"() {
|
|
10960
|
+
"use strict";
|
|
10961
|
+
init_esm_shims();
|
|
10962
|
+
METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
10963
|
+
isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10964
|
+
}
|
|
10965
|
+
});
|
|
10966
|
+
|
|
10967
|
+
// src/api/check.ts
|
|
10968
|
+
var check_exports = {};
|
|
10969
|
+
__export(check_exports, {
|
|
10970
|
+
findSpec: () => findSpec,
|
|
10971
|
+
runApiCheck: () => runApiCheck
|
|
10972
|
+
});
|
|
10973
|
+
import { execFile as execFile4 } from "child_process";
|
|
10974
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
|
|
10975
|
+
import { relative as relative4, resolve } from "path";
|
|
10976
|
+
import { promisify as promisify4 } from "util";
|
|
10977
|
+
function findSpec(dir) {
|
|
10978
|
+
for (const candidate of CANDIDATES2) {
|
|
10979
|
+
const full = resolve(dir, candidate);
|
|
10980
|
+
if (existsSync7(full)) return full;
|
|
10981
|
+
}
|
|
10982
|
+
return null;
|
|
10983
|
+
}
|
|
10984
|
+
async function showAtRevision(dir, rev, repoPath) {
|
|
10985
|
+
try {
|
|
10986
|
+
const { stdout } = await execFileP3("git", ["show", `${rev}:${repoPath}`], {
|
|
10987
|
+
cwd: dir,
|
|
10988
|
+
maxBuffer: 32 * 1024 * 1024
|
|
10989
|
+
});
|
|
10990
|
+
return stdout;
|
|
10991
|
+
} catch {
|
|
10992
|
+
return null;
|
|
10993
|
+
}
|
|
10994
|
+
}
|
|
10995
|
+
async function runApiCheck(dir, opts = {}) {
|
|
10996
|
+
const against = opts.against ?? "HEAD";
|
|
10997
|
+
const specPath = opts.spec ? resolve(dir, opts.spec) : findSpec(dir);
|
|
10998
|
+
const spec = specPath ? relative4(dir, specPath) || specPath : "(none found)";
|
|
10999
|
+
const base = { spec, against, title: null, fromVersion: null, toVersion: null };
|
|
11000
|
+
if (!specPath) {
|
|
11001
|
+
return {
|
|
11002
|
+
...base,
|
|
11003
|
+
verdict: "inconclusive",
|
|
11004
|
+
versionCovers: null,
|
|
11005
|
+
diff: {
|
|
11006
|
+
breaking: [],
|
|
11007
|
+
other: [],
|
|
11008
|
+
inconclusive: `no OpenAPI document found in ${dir}. Name one: lurq check-api <file>`
|
|
11009
|
+
}
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
const current = extractApiSurface(readFileSync9(specPath, "utf8"), spec);
|
|
11013
|
+
const previousSource = await showAtRevision(dir, against, spec);
|
|
11014
|
+
if (previousSource === null) {
|
|
11015
|
+
return {
|
|
11016
|
+
...base,
|
|
11017
|
+
title: current.title,
|
|
11018
|
+
toVersion: current.version,
|
|
11019
|
+
verdict: "inconclusive",
|
|
11020
|
+
versionCovers: null,
|
|
11021
|
+
diff: {
|
|
11022
|
+
breaking: [],
|
|
11023
|
+
other: [],
|
|
11024
|
+
inconclusive: `${spec} does not exist at ${against}, so there is nothing to compare against`
|
|
11025
|
+
}
|
|
11026
|
+
};
|
|
11027
|
+
}
|
|
11028
|
+
const previous = extractApiSurface(previousSource, `${spec} at ${against}`);
|
|
11029
|
+
return { ...base, ...checkApi(previous, current) };
|
|
11030
|
+
}
|
|
11031
|
+
var execFileP3, CANDIDATES2;
|
|
11032
|
+
var init_check2 = __esm({
|
|
11033
|
+
"src/api/check.ts"() {
|
|
11034
|
+
"use strict";
|
|
11035
|
+
init_esm_shims();
|
|
11036
|
+
init_diff3();
|
|
11037
|
+
init_openapi();
|
|
11038
|
+
execFileP3 = promisify4(execFile4);
|
|
11039
|
+
CANDIDATES2 = [
|
|
11040
|
+
"openapi.yaml",
|
|
11041
|
+
"openapi.yml",
|
|
11042
|
+
"openapi.json",
|
|
11043
|
+
"swagger.yaml",
|
|
11044
|
+
"swagger.yml",
|
|
11045
|
+
"swagger.json",
|
|
11046
|
+
"api/openapi.yaml",
|
|
11047
|
+
"api/openapi.yml",
|
|
11048
|
+
"api/openapi.json",
|
|
11049
|
+
"docs/openapi.yaml",
|
|
11050
|
+
"docs/openapi.yml"
|
|
11051
|
+
];
|
|
11052
|
+
}
|
|
11053
|
+
});
|
|
11054
|
+
|
|
9904
11055
|
// src/graph/types.ts
|
|
9905
11056
|
function canonicalKey(ref) {
|
|
9906
11057
|
return `${ref.kind}:${ref.namespace}:${ref.name}:${ref.version ?? ""}`;
|
|
@@ -9913,7 +11064,7 @@ var init_types6 = __esm({
|
|
|
9913
11064
|
});
|
|
9914
11065
|
|
|
9915
11066
|
// src/db/graph.ts
|
|
9916
|
-
import { and as
|
|
11067
|
+
import { and as and13, eq as eq16, isNull as isNull3 } from "drizzle-orm";
|
|
9917
11068
|
import { createHash as createHash6 } from "crypto";
|
|
9918
11069
|
var init_graph = __esm({
|
|
9919
11070
|
"src/db/graph.ts"() {
|
|
@@ -9925,7 +11076,7 @@ var init_graph = __esm({
|
|
|
9925
11076
|
});
|
|
9926
11077
|
|
|
9927
11078
|
// src/db/surface.ts
|
|
9928
|
-
import { and as
|
|
11079
|
+
import { and as and14, desc as desc5, eq as eq17, sql as sql11 } from "drizzle-orm";
|
|
9929
11080
|
function surfaceRef(pkg, version) {
|
|
9930
11081
|
return { kind: "package_surface", namespace: "npm", name: pkg, version };
|
|
9931
11082
|
}
|
|
@@ -9946,7 +11097,7 @@ var init_surface = __esm({
|
|
|
9946
11097
|
|
|
9947
11098
|
// src/mcp/surfaceHandlers.ts
|
|
9948
11099
|
import { createHash as createHash7 } from "crypto";
|
|
9949
|
-
import { and as
|
|
11100
|
+
import { and as and15, desc as desc6, eq as eq18 } from "drizzle-orm";
|
|
9950
11101
|
function rowsToSurface(pkg, version, rows, tier) {
|
|
9951
11102
|
const symbolsOut = rows.map((r) => ({
|
|
9952
11103
|
path: r.path,
|
|
@@ -9970,17 +11121,17 @@ function rowsToSurface(pkg, version, rows, tier) {
|
|
|
9970
11121
|
async function loadStored(db, pkg, version, tenantId = 0) {
|
|
9971
11122
|
let entity;
|
|
9972
11123
|
if (version === null) {
|
|
9973
|
-
const rows2 = await db.select({ e: entities, at: observations.observedAt }).from(entities).innerJoin(claims,
|
|
11124
|
+
const rows2 = await db.select({ e: entities, at: observations.observedAt }).from(entities).innerJoin(claims, eq18(claims.subjectId, entities.id)).innerJoin(observations, eq18(observations.claimId, claims.id)).where(and15(eq18(entities.kind, "package_surface"), eq18(entities.name, pkg))).orderBy(desc6(observations.observedAt)).limit(1);
|
|
9974
11125
|
entity = rows2.find((r) => r.e.tenantId === tenantId)?.e;
|
|
9975
11126
|
} else {
|
|
9976
11127
|
const key = canonicalKey(surfaceRef(pkg, version));
|
|
9977
|
-
const ents = await db.select().from(entities).where(
|
|
11128
|
+
const ents = await db.select().from(entities).where(eq18(entities.canonicalKey, key)).limit(2);
|
|
9978
11129
|
entity = ents.find((e) => e.tenantId === tenantId);
|
|
9979
11130
|
}
|
|
9980
11131
|
if (!entity) return null;
|
|
9981
|
-
const obs = await db.select({ o: observations }).from(observations).innerJoin(claims,
|
|
11132
|
+
const obs = await db.select({ o: observations }).from(observations).innerJoin(claims, eq18(observations.claimId, claims.id)).where(eq18(claims.subjectId, entity.id)).orderBy(desc6(observations.observedAt)).limit(1);
|
|
9982
11133
|
const latest = obs[0]?.o ?? null;
|
|
9983
|
-
const rows = await db.select().from(symbols).where(
|
|
11134
|
+
const rows = await db.select().from(symbols).where(eq18(symbols.entityId, entity.id));
|
|
9984
11135
|
return {
|
|
9985
11136
|
entityId: entity.id,
|
|
9986
11137
|
rows,
|
|
@@ -10052,18 +11203,18 @@ async function diffSurfaceUncached(db, input) {
|
|
|
10052
11203
|
loadStored(db, input.package, input.fromVersion),
|
|
10053
11204
|
loadStored(db, input.package, input.toVersion)
|
|
10054
11205
|
]);
|
|
10055
|
-
const
|
|
10056
|
-
if (!a || a.rows.length === 0)
|
|
10057
|
-
if (!b || b.rows.length === 0)
|
|
10058
|
-
if (
|
|
10059
|
-
for (const v of
|
|
11206
|
+
const missing2 = [];
|
|
11207
|
+
if (!a || a.rows.length === 0) missing2.push(input.fromVersion);
|
|
11208
|
+
if (!b || b.rows.length === 0) missing2.push(input.toVersion);
|
|
11209
|
+
if (missing2.length) {
|
|
11210
|
+
for (const v of missing2) await enqueueSurface(db, input.package, v).catch(() => {
|
|
10060
11211
|
});
|
|
10061
11212
|
return {
|
|
10062
11213
|
package: input.package,
|
|
10063
11214
|
fromVersion: input.fromVersion,
|
|
10064
11215
|
toVersion: input.toVersion,
|
|
10065
11216
|
verdict: "unknown",
|
|
10066
|
-
inconclusive: `no extracted surface for ${
|
|
11217
|
+
inconclusive: `no extracted surface for ${missing2.join(", ")}, queued; retry shortly. NOT evidence that symbols were removed.`,
|
|
10067
11218
|
removed: [],
|
|
10068
11219
|
added: [],
|
|
10069
11220
|
arityChanged: [],
|
|
@@ -10208,7 +11359,7 @@ var init_toolDescriptions = __esm({
|
|
|
10208
11359
|
});
|
|
10209
11360
|
|
|
10210
11361
|
// src/db/usage.ts
|
|
10211
|
-
import { and as
|
|
11362
|
+
import { and as and16, desc as desc7, eq as eq19, gte as gte2, sql as sql12 } from "drizzle-orm";
|
|
10212
11363
|
function windowStart(days) {
|
|
10213
11364
|
return sql12`CURRENT_DATE - (${days}::int - 1)`;
|
|
10214
11365
|
}
|
|
@@ -10242,9 +11393,9 @@ async function getUsageByTool(db, ownerId, days) {
|
|
|
10242
11393
|
tool: ownerUsageDaily.tool,
|
|
10243
11394
|
count: sql12`sum(${ownerUsageDaily.count})::int`
|
|
10244
11395
|
}).from(ownerUsageDaily).where(
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
11396
|
+
and16(
|
|
11397
|
+
eq19(ownerUsageDaily.ownerId, ownerId),
|
|
11398
|
+
gte2(ownerUsageDaily.date, windowStart(days))
|
|
10248
11399
|
)
|
|
10249
11400
|
).groupBy(ownerUsageDaily.tool).orderBy(desc7(sql12`sum(${ownerUsageDaily.count})`));
|
|
10250
11401
|
return rows.map((r) => ({ tool: r.tool, count: Number(r.count) }));
|
|
@@ -10409,9 +11560,20 @@ function buildMcpServer(db, ctx = {}) {
|
|
|
10409
11560
|
async (args) => json(await run("diff_surface", () => handleDiffSurface(db, args)))
|
|
10410
11561
|
);
|
|
10411
11562
|
server.registerTool(
|
|
10412
|
-
"
|
|
11563
|
+
"capabilities",
|
|
10413
11564
|
{
|
|
10414
|
-
title: "
|
|
11565
|
+
title: "What lurq can do",
|
|
11566
|
+
description: "Look up which lurq tool answers a situation, and what to run next. Call when you're unsure whether lurq covers something (an upgrade, a licence rule, a version's exact exports, publishing a package) instead of guessing or skipping it. Returns matching capabilities with the tool or command to use.",
|
|
11567
|
+
inputSchema: {
|
|
11568
|
+
query: z2.string().max(300).optional().describe("What you are trying to do, in plain words. Omit for the full menu.")
|
|
11569
|
+
}
|
|
11570
|
+
},
|
|
11571
|
+
async (args) => json({ capabilities: searchCapabilities(args.query ?? "", 6) })
|
|
11572
|
+
);
|
|
11573
|
+
server.registerTool(
|
|
11574
|
+
"report_outcome",
|
|
11575
|
+
{
|
|
11576
|
+
title: "Report a recommendation outcome",
|
|
10415
11577
|
description: "Opt-in feedback after acting on a lurq recommendation: report whether you went with the package and whether it built. No source code, only the coarse decision + a build signal. Helps lurq learn which packages agents actually succeed with; safe to skip.",
|
|
10416
11578
|
inputSchema: {
|
|
10417
11579
|
package: npmName.describe("The package that was recommended"),
|
|
@@ -10449,6 +11611,7 @@ var init_server = __esm({
|
|
|
10449
11611
|
init_esm_shims();
|
|
10450
11612
|
init_constants();
|
|
10451
11613
|
init_types();
|
|
11614
|
+
init_capabilities();
|
|
10452
11615
|
init_client();
|
|
10453
11616
|
init_logger();
|
|
10454
11617
|
init_surfaceHandlers();
|
|
@@ -10472,7 +11635,7 @@ var init_server = __esm({
|
|
|
10472
11635
|
|
|
10473
11636
|
// src/auth/apiKeys.ts
|
|
10474
11637
|
import { createHash as createHash8, randomBytes } from "crypto";
|
|
10475
|
-
import { and as
|
|
11638
|
+
import { and as and17, desc as desc8, eq as eq20, isNull as isNull4 } from "drizzle-orm";
|
|
10476
11639
|
function hashKey(key) {
|
|
10477
11640
|
return createHash8("sha256").update(key).digest("hex");
|
|
10478
11641
|
}
|
|
@@ -10495,7 +11658,7 @@ async function createKey(db, input = {}) {
|
|
|
10495
11658
|
function stampLastUsed(db, entry, now) {
|
|
10496
11659
|
if (now - entry.lastStampAt < STAMP_INTERVAL_MS) return;
|
|
10497
11660
|
entry.lastStampAt = now;
|
|
10498
|
-
db.update(apiKeys).set({ lastUsedAt: new Date(now) }).where(
|
|
11661
|
+
db.update(apiKeys).set({ lastUsedAt: new Date(now) }).where(eq20(apiKeys.id, entry.row.id)).then(void 0, (err) => logger.debug(`lastUsedAt stamp failed: ${String(err)}`));
|
|
10499
11662
|
}
|
|
10500
11663
|
async function lookupActiveKey(db, key) {
|
|
10501
11664
|
const hash = hashKey(key);
|
|
@@ -10505,7 +11668,7 @@ async function lookupActiveKey(db, key) {
|
|
|
10505
11668
|
stampLastUsed(db, cached3, now);
|
|
10506
11669
|
return cached3.row;
|
|
10507
11670
|
}
|
|
10508
|
-
const [row] = await db.select().from(apiKeys).where(
|
|
11671
|
+
const [row] = await db.select().from(apiKeys).where(and17(eq20(apiKeys.keyHash, hash), isNull4(apiKeys.revokedAt))).limit(1);
|
|
10509
11672
|
if (!row) {
|
|
10510
11673
|
authCache.delete(hash);
|
|
10511
11674
|
return null;
|
|
@@ -10516,30 +11679,35 @@ async function lookupActiveKey(db, key) {
|
|
|
10516
11679
|
return row;
|
|
10517
11680
|
}
|
|
10518
11681
|
async function listKeysForOwner(db, ownerId) {
|
|
10519
|
-
return db.select().from(apiKeys).where(
|
|
11682
|
+
return db.select().from(apiKeys).where(eq20(apiKeys.ownerId, ownerId)).orderBy(desc8(apiKeys.createdAt));
|
|
10520
11683
|
}
|
|
10521
11684
|
function matchByPrefixOrId(prefixOrId) {
|
|
10522
11685
|
const asId = Number(prefixOrId);
|
|
10523
|
-
return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ?
|
|
11686
|
+
return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq20(apiKeys.id, asId) : eq20(apiKeys.prefix, prefixOrId);
|
|
10524
11687
|
}
|
|
10525
11688
|
async function findKeyForOwner(db, args) {
|
|
10526
|
-
const [row] = await db.select().from(apiKeys).where(
|
|
10527
|
-
|
|
10528
|
-
|
|
11689
|
+
const [row] = await db.select().from(apiKeys).where(
|
|
11690
|
+
and17(
|
|
11691
|
+
matchByPrefixOrId(args.prefixOrId),
|
|
11692
|
+
eq20(apiKeys.ownerId, args.ownerId),
|
|
11693
|
+
isNull4(apiKeys.revokedAt)
|
|
11694
|
+
)
|
|
11695
|
+
).orderBy(apiKeys.id).limit(1);
|
|
11696
|
+
return row ?? null;
|
|
10529
11697
|
}
|
|
10530
11698
|
async function revokeKey(db, prefixOrId) {
|
|
10531
|
-
const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
11699
|
+
const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and17(matchByPrefixOrId(prefixOrId), isNull4(apiKeys.revokedAt))).returning({ id: apiKeys.id });
|
|
10532
11700
|
return rows.length;
|
|
10533
11701
|
}
|
|
10534
11702
|
async function rotateKey(db, prefixOrId) {
|
|
10535
|
-
const [previous] = await db.select().from(apiKeys).where(
|
|
11703
|
+
const [previous] = await db.select().from(apiKeys).where(and17(matchByPrefixOrId(prefixOrId), isNull4(apiKeys.revokedAt))).limit(1);
|
|
10536
11704
|
if (!previous) return null;
|
|
10537
11705
|
const { key, row } = await createKey(db, {
|
|
10538
11706
|
label: previous.label ?? void 0,
|
|
10539
11707
|
tier: previous.tier,
|
|
10540
11708
|
ownerId: previous.ownerId ?? void 0
|
|
10541
11709
|
});
|
|
10542
|
-
await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
11710
|
+
await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq20(apiKeys.id, previous.id));
|
|
10543
11711
|
return { key, row, previous };
|
|
10544
11712
|
}
|
|
10545
11713
|
var DISPLAY_BODY, authCache, AUTH_TTL_MS, STAMP_INTERVAL_MS;
|
|
@@ -10620,17 +11788,411 @@ var init_parse = __esm({
|
|
|
10620
11788
|
}
|
|
10621
11789
|
});
|
|
10622
11790
|
|
|
11791
|
+
// src/policy/conformance.ts
|
|
11792
|
+
import { and as and18, eq as eq21, inArray as inArray9 } from "drizzle-orm";
|
|
11793
|
+
async function loadRuleFacts(db, names2, ecosystem = DEFAULT_ECOSYSTEM) {
|
|
11794
|
+
const out = /* @__PURE__ */ new Map();
|
|
11795
|
+
for (let i = 0; i < names2.length; i += NAME_CHUNK2) {
|
|
11796
|
+
const rows = await db.select({
|
|
11797
|
+
name: packages.name,
|
|
11798
|
+
license: packages.license,
|
|
11799
|
+
deprecated: packages.deprecated,
|
|
11800
|
+
confidence: packages.confidence
|
|
11801
|
+
}).from(packages).where(
|
|
11802
|
+
and18(
|
|
11803
|
+
inArray9(packages.name, names2.slice(i, i + NAME_CHUNK2)),
|
|
11804
|
+
eq21(packages.ecosystem, ecosystem)
|
|
11805
|
+
)
|
|
11806
|
+
);
|
|
11807
|
+
for (const row of rows) {
|
|
11808
|
+
out.set(row.name, {
|
|
11809
|
+
license: row.license,
|
|
11810
|
+
deprecated: row.deprecated,
|
|
11811
|
+
confidence: row.confidence
|
|
11812
|
+
});
|
|
11813
|
+
}
|
|
11814
|
+
}
|
|
11815
|
+
return out;
|
|
11816
|
+
}
|
|
11817
|
+
function ruleRepo(policy, names2, facts) {
|
|
11818
|
+
const violations = [];
|
|
11819
|
+
let checked = 0;
|
|
11820
|
+
let unchecked = 0;
|
|
11821
|
+
let unscored = 0;
|
|
11822
|
+
for (const name of names2) {
|
|
11823
|
+
const fact = facts.get(name);
|
|
11824
|
+
if (!fact) {
|
|
11825
|
+
unchecked += 1;
|
|
11826
|
+
continue;
|
|
11827
|
+
}
|
|
11828
|
+
checked += 1;
|
|
11829
|
+
if (policy.minConfidence !== null && fact.confidence === null) unscored += 1;
|
|
11830
|
+
const exclusion = check(
|
|
11831
|
+
policy,
|
|
11832
|
+
// `check` needs a grade and an unscored package has none. The top grade
|
|
11833
|
+
// makes the confidence floor abstain rather than convict on a fact never
|
|
11834
|
+
// established — "absent facts never convict", in enforce.ts — while the
|
|
11835
|
+
// licence and deprecation rules still apply normally. The abstention is
|
|
11836
|
+
// counted above rather than hidden.
|
|
11837
|
+
{ name, confidence: fact.confidence ?? "proven" },
|
|
11838
|
+
{ license: fact.license, deprecated: fact.deprecated }
|
|
11839
|
+
);
|
|
11840
|
+
if (exclusion) violations.push(exclusion);
|
|
11841
|
+
}
|
|
11842
|
+
return {
|
|
11843
|
+
checked,
|
|
11844
|
+
unchecked,
|
|
11845
|
+
unscored,
|
|
11846
|
+
total: violations.length,
|
|
11847
|
+
violations: violations.slice(0, VIOLATION_CAP)
|
|
11848
|
+
};
|
|
11849
|
+
}
|
|
11850
|
+
function bySeverity(a, z3) {
|
|
11851
|
+
return z3.total - a.total || z3.unchecked - a.unchecked || a.fullName.localeCompare(z3.fullName);
|
|
11852
|
+
}
|
|
11853
|
+
async function repoConformance(db, ownerId) {
|
|
11854
|
+
const policy = await getSelectionPolicy(db, ownerId);
|
|
11855
|
+
if (!hasRules(policy)) return { enforcing: false, repos: [] };
|
|
11856
|
+
const rows = await db.select({ id: repos.id, fullName: repos.fullName, manifests: repos.manifests }).from(repos).where(eq21(repos.ownerId, ownerId));
|
|
11857
|
+
if (!rows.length) return { enforcing: true, repos: [] };
|
|
11858
|
+
const declaredPerRepo = rows.map((r) => ({
|
|
11859
|
+
repoId: r.id,
|
|
11860
|
+
fullName: r.fullName,
|
|
11861
|
+
names: [...declaredDeps(r.manifests ?? []).keys()]
|
|
11862
|
+
}));
|
|
11863
|
+
const union = [...new Set(declaredPerRepo.flatMap((r) => r.names))];
|
|
11864
|
+
const facts = await loadRuleFacts(db, union);
|
|
11865
|
+
const report = declaredPerRepo.map(({ repoId, fullName, names: names2 }) => ({
|
|
11866
|
+
repoId,
|
|
11867
|
+
fullName,
|
|
11868
|
+
...ruleRepo(policy, names2, facts)
|
|
11869
|
+
}));
|
|
11870
|
+
return { enforcing: true, repos: report.sort(bySeverity) };
|
|
11871
|
+
}
|
|
11872
|
+
var NAME_CHUNK2, VIOLATION_CAP;
|
|
11873
|
+
var init_conformance = __esm({
|
|
11874
|
+
"src/policy/conformance.ts"() {
|
|
11875
|
+
"use strict";
|
|
11876
|
+
init_esm_shims();
|
|
11877
|
+
init_types();
|
|
11878
|
+
init_schema();
|
|
11879
|
+
init_selectionPolicy();
|
|
11880
|
+
init_drift();
|
|
11881
|
+
init_enforce();
|
|
11882
|
+
NAME_CHUNK2 = 500;
|
|
11883
|
+
VIOLATION_CAP = 50;
|
|
11884
|
+
}
|
|
11885
|
+
});
|
|
11886
|
+
|
|
11887
|
+
// src/core/plans.ts
|
|
11888
|
+
function planFor(tier) {
|
|
11889
|
+
return PLANS[tier ?? "free"] ?? PLANS.free;
|
|
11890
|
+
}
|
|
11891
|
+
function isServed(status) {
|
|
11892
|
+
return status != null && SERVED_STATUSES.has(status);
|
|
11893
|
+
}
|
|
11894
|
+
var TIERS, PLANS, PLAN_LIST, SERVED_STATUSES;
|
|
11895
|
+
var init_plans = __esm({
|
|
11896
|
+
"src/core/plans.ts"() {
|
|
11897
|
+
"use strict";
|
|
11898
|
+
init_esm_shims();
|
|
11899
|
+
TIERS = ["free", "pro", "enterprise"];
|
|
11900
|
+
PLANS = {
|
|
11901
|
+
free: {
|
|
11902
|
+
tier: "free",
|
|
11903
|
+
name: "Free",
|
|
11904
|
+
priceCents: 0,
|
|
11905
|
+
monthlyCalls: 200,
|
|
11906
|
+
ratePerMinute: 60,
|
|
11907
|
+
tagline: "Enough to find out whether the index is telling the truth.",
|
|
11908
|
+
features: [
|
|
11909
|
+
"CLI and installable skill",
|
|
11910
|
+
"200 hosted calls a month",
|
|
11911
|
+
"recommend, evaluate, verify",
|
|
11912
|
+
"Daily refreshed index",
|
|
11913
|
+
"Community support"
|
|
11914
|
+
],
|
|
11915
|
+
paid: false
|
|
11916
|
+
},
|
|
11917
|
+
pro: {
|
|
11918
|
+
tier: "pro",
|
|
11919
|
+
name: "Pro",
|
|
11920
|
+
priceCents: 500,
|
|
11921
|
+
monthlyCalls: 1e4,
|
|
11922
|
+
ratePerMinute: 120,
|
|
11923
|
+
tagline: "For one developer who runs it on every install.",
|
|
11924
|
+
features: [
|
|
11925
|
+
"10,000 hosted calls a month",
|
|
11926
|
+
"compare, diagram and plan",
|
|
11927
|
+
"Repo autopilot and drift alerts",
|
|
11928
|
+
"Priority index freshness",
|
|
11929
|
+
"Email support"
|
|
11930
|
+
],
|
|
11931
|
+
paid: true
|
|
11932
|
+
},
|
|
11933
|
+
enterprise: {
|
|
11934
|
+
tier: "enterprise",
|
|
11935
|
+
name: "Enterprise",
|
|
11936
|
+
priceCents: 1e4,
|
|
11937
|
+
monthlyCalls: null,
|
|
11938
|
+
ratePerMinute: 600,
|
|
11939
|
+
tagline: "For a team that needs the graph under its own controls.",
|
|
11940
|
+
features: [
|
|
11941
|
+
"Uncapped hosted calls",
|
|
11942
|
+
"SSO and audit logs",
|
|
11943
|
+
"Private compatibility runs",
|
|
11944
|
+
"Custom SLA",
|
|
11945
|
+
"Dedicated support channel"
|
|
11946
|
+
],
|
|
11947
|
+
paid: true,
|
|
11948
|
+
contactOnly: true
|
|
11949
|
+
}
|
|
11950
|
+
};
|
|
11951
|
+
PLAN_LIST = TIERS.map((t) => PLANS[t]);
|
|
11952
|
+
SERVED_STATUSES = /* @__PURE__ */ new Set(["active", "trialing", "past_due"]);
|
|
11953
|
+
}
|
|
11954
|
+
});
|
|
11955
|
+
|
|
11956
|
+
// src/db/subscriptions.ts
|
|
11957
|
+
import { and as and19, eq as eq22, gte as gte3, sql as sql13 } from "drizzle-orm";
|
|
11958
|
+
async function getSubscription(db, ownerId) {
|
|
11959
|
+
const rows = await db.select().from(subscriptions).where(eq22(subscriptions.ownerId, ownerId)).limit(1);
|
|
11960
|
+
return rows[0] ?? null;
|
|
11961
|
+
}
|
|
11962
|
+
async function getSubscriptionByCustomer(db, stripeCustomerId) {
|
|
11963
|
+
const rows = await db.select().from(subscriptions).where(eq22(subscriptions.stripeCustomerId, stripeCustomerId)).limit(1);
|
|
11964
|
+
return rows[0] ?? null;
|
|
11965
|
+
}
|
|
11966
|
+
async function linkCustomer(db, ownerId, stripeCustomerId) {
|
|
11967
|
+
await db.insert(subscriptions).values({ ownerId, stripeCustomerId }).onConflictDoUpdate({
|
|
11968
|
+
target: subscriptions.ownerId,
|
|
11969
|
+
set: { stripeCustomerId, updatedAt: /* @__PURE__ */ new Date() }
|
|
11970
|
+
});
|
|
11971
|
+
}
|
|
11972
|
+
async function applySubscriptionEvent(db, stripeCustomerId, update) {
|
|
11973
|
+
const result = await db.insert(subscriptions).values({
|
|
11974
|
+
// A webhook can legitimately arrive for a customer we have no row for
|
|
11975
|
+
// (created in the Stripe dashboard, or our insert lost a race with
|
|
11976
|
+
// checkout). ownerId comes from customer metadata in that case; the
|
|
11977
|
+
// caller resolves it and passes a row that already exists, so this
|
|
11978
|
+
// conflict target is the customer, not the owner.
|
|
11979
|
+
ownerId: sql13`coalesce(
|
|
11980
|
+
(select s.owner_id from subscriptions s where s.stripe_customer_id = ${stripeCustomerId}),
|
|
11981
|
+
${"stripe:" + stripeCustomerId}
|
|
11982
|
+
)`,
|
|
11983
|
+
stripeCustomerId,
|
|
11984
|
+
stripeSubscriptionId: update.stripeSubscriptionId,
|
|
11985
|
+
tier: update.tier,
|
|
11986
|
+
status: update.status,
|
|
11987
|
+
currentPeriodEnd: update.currentPeriodEnd,
|
|
11988
|
+
cancelAtPeriodEnd: update.cancelAtPeriodEnd,
|
|
11989
|
+
lastEventAt: update.eventAt,
|
|
11990
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
11991
|
+
}).onConflictDoUpdate({
|
|
11992
|
+
target: subscriptions.stripeCustomerId,
|
|
11993
|
+
set: {
|
|
11994
|
+
stripeSubscriptionId: update.stripeSubscriptionId,
|
|
11995
|
+
tier: update.tier,
|
|
11996
|
+
status: update.status,
|
|
11997
|
+
currentPeriodEnd: update.currentPeriodEnd,
|
|
11998
|
+
cancelAtPeriodEnd: update.cancelAtPeriodEnd,
|
|
11999
|
+
lastEventAt: update.eventAt,
|
|
12000
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
12001
|
+
},
|
|
12002
|
+
setWhere: sql13`${subscriptions.lastEventAt} is null or ${subscriptions.lastEventAt} <= ${update.eventAt}`
|
|
12003
|
+
}).returning({ ownerId: subscriptions.ownerId });
|
|
12004
|
+
return result.length > 0;
|
|
12005
|
+
}
|
|
12006
|
+
async function monthlyCallCount(db, ownerId) {
|
|
12007
|
+
const rows = await db.select({ total: sql13`coalesce(sum(${ownerUsageDaily.count}), 0)` }).from(ownerUsageDaily).where(
|
|
12008
|
+
and19(
|
|
12009
|
+
eq22(ownerUsageDaily.ownerId, ownerId),
|
|
12010
|
+
gte3(ownerUsageDaily.date, sql13`date_trunc('month', CURRENT_DATE)::date`)
|
|
12011
|
+
)
|
|
12012
|
+
);
|
|
12013
|
+
return Number(rows[0]?.total ?? 0);
|
|
12014
|
+
}
|
|
12015
|
+
async function entitlementFor(db, ownerId) {
|
|
12016
|
+
if (!ownerId) {
|
|
12017
|
+
return { plan: planFor("enterprise"), used: 0, withinQuota: true };
|
|
12018
|
+
}
|
|
12019
|
+
const [sub, used] = await Promise.all([
|
|
12020
|
+
getSubscription(db, ownerId),
|
|
12021
|
+
monthlyCallCount(db, ownerId)
|
|
12022
|
+
]);
|
|
12023
|
+
const plan = planFor(sub && isServed(sub.status) ? sub.tier : "free");
|
|
12024
|
+
const withinQuota = plan.monthlyCalls === null || used < plan.monthlyCalls;
|
|
12025
|
+
return { plan, used, withinQuota };
|
|
12026
|
+
}
|
|
12027
|
+
var init_subscriptions = __esm({
|
|
12028
|
+
"src/db/subscriptions.ts"() {
|
|
12029
|
+
"use strict";
|
|
12030
|
+
init_esm_shims();
|
|
12031
|
+
init_plans();
|
|
12032
|
+
init_schema();
|
|
12033
|
+
}
|
|
12034
|
+
});
|
|
12035
|
+
|
|
12036
|
+
// src/billing/stripe.ts
|
|
12037
|
+
async function stripeClient() {
|
|
12038
|
+
const { STRIPE_SECRET_KEY } = getConfig();
|
|
12039
|
+
if (!STRIPE_SECRET_KEY) return null;
|
|
12040
|
+
if (!clientPromise2) {
|
|
12041
|
+
clientPromise2 = (async () => {
|
|
12042
|
+
const { default: StripeCtor } = await import("stripe");
|
|
12043
|
+
return new StripeCtor(STRIPE_SECRET_KEY, {
|
|
12044
|
+
apiVersion: API_VERSION,
|
|
12045
|
+
// Retries on Stripe's side of a network blip. The default is 0, which
|
|
12046
|
+
// turns one dropped connection into a failed checkout for a real person.
|
|
12047
|
+
maxNetworkRetries: 2,
|
|
12048
|
+
timeout: 15e3
|
|
12049
|
+
});
|
|
12050
|
+
})();
|
|
12051
|
+
}
|
|
12052
|
+
return clientPromise2;
|
|
12053
|
+
}
|
|
12054
|
+
function billingEnabled() {
|
|
12055
|
+
return Boolean(getConfig().STRIPE_SECRET_KEY);
|
|
12056
|
+
}
|
|
12057
|
+
function priceIdFor(tier) {
|
|
12058
|
+
const config = getConfig();
|
|
12059
|
+
if (tier === "pro") return config.STRIPE_PRICE_PRO ?? null;
|
|
12060
|
+
if (tier === "enterprise") return config.STRIPE_PRICE_ENTERPRISE ?? null;
|
|
12061
|
+
return null;
|
|
12062
|
+
}
|
|
12063
|
+
function tierForPrice(priceId, metadataTier) {
|
|
12064
|
+
const config = getConfig();
|
|
12065
|
+
if (priceId && priceId === config.STRIPE_PRICE_PRO) return "pro";
|
|
12066
|
+
if (priceId && priceId === config.STRIPE_PRICE_ENTERPRISE) return "enterprise";
|
|
12067
|
+
if (metadataTier && metadataTier in PLANS) return metadataTier;
|
|
12068
|
+
if (priceId) {
|
|
12069
|
+
logger.warn(`billing: price ${priceId} maps to no configured tier; treating as free`);
|
|
12070
|
+
}
|
|
12071
|
+
return "free";
|
|
12072
|
+
}
|
|
12073
|
+
async function customerFor(db, ownerId, email) {
|
|
12074
|
+
const stripe = await stripeClient();
|
|
12075
|
+
if (!stripe) return null;
|
|
12076
|
+
const existing = await getSubscription(db, ownerId);
|
|
12077
|
+
if (existing?.stripeCustomerId) return existing.stripeCustomerId;
|
|
12078
|
+
const customer = await stripe.customers.create({
|
|
12079
|
+
...email ? { email } : {},
|
|
12080
|
+
metadata: { ownerId }
|
|
12081
|
+
});
|
|
12082
|
+
await linkCustomer(db, ownerId, customer.id);
|
|
12083
|
+
return customer.id;
|
|
12084
|
+
}
|
|
12085
|
+
async function createCheckoutSession(db, req) {
|
|
12086
|
+
const stripe = await stripeClient();
|
|
12087
|
+
if (!stripe) return null;
|
|
12088
|
+
const price = priceIdFor(req.tier);
|
|
12089
|
+
if (!price) return null;
|
|
12090
|
+
const customer = await customerFor(db, req.ownerId, req.email);
|
|
12091
|
+
if (!customer) return null;
|
|
12092
|
+
const base = getConfig().LURQ_WEB_URL.replace(/\/$/, "");
|
|
12093
|
+
const session = await stripe.checkout.sessions.create({
|
|
12094
|
+
mode: "subscription",
|
|
12095
|
+
customer,
|
|
12096
|
+
line_items: [{ price, quantity: 1 }],
|
|
12097
|
+
// Stripe substitutes the real id. The dashboard reads it to poll for the
|
|
12098
|
+
// webhook having landed, so the page after payment can say "active" rather
|
|
12099
|
+
// than "we think so".
|
|
12100
|
+
success_url: `${base}/dashboard/billing?checkout={CHECKOUT_SESSION_ID}`,
|
|
12101
|
+
cancel_url: `${base}/#pricing`,
|
|
12102
|
+
// Carried onto the Subscription so tierForPrice has a fallback if the Price
|
|
12103
|
+
// is later swapped out from under us.
|
|
12104
|
+
subscription_data: { metadata: { ownerId: req.ownerId, tier: req.tier } },
|
|
12105
|
+
metadata: { ownerId: req.ownerId, tier: req.tier },
|
|
12106
|
+
allow_promotion_codes: true,
|
|
12107
|
+
// Stripe Tax works out VAT/sales tax per the customer's address. This is the
|
|
12108
|
+
// half of merchant-of-record we can buy; registration and remittance are
|
|
12109
|
+
// still ours, which is the tradeoff taken when Stripe was chosen over a MoR.
|
|
12110
|
+
automatic_tax: { enabled: true },
|
|
12111
|
+
billing_address_collection: "auto"
|
|
12112
|
+
});
|
|
12113
|
+
return session.url;
|
|
12114
|
+
}
|
|
12115
|
+
async function createPortalSession(db, ownerId) {
|
|
12116
|
+
const stripe = await stripeClient();
|
|
12117
|
+
if (!stripe) return null;
|
|
12118
|
+
const sub = await getSubscription(db, ownerId);
|
|
12119
|
+
if (!sub?.stripeCustomerId) return null;
|
|
12120
|
+
const base = getConfig().LURQ_WEB_URL.replace(/\/$/, "");
|
|
12121
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
12122
|
+
customer: sub.stripeCustomerId,
|
|
12123
|
+
return_url: `${base}/dashboard/billing`
|
|
12124
|
+
});
|
|
12125
|
+
return session.url;
|
|
12126
|
+
}
|
|
12127
|
+
async function constructEvent(raw, signature) {
|
|
12128
|
+
const stripe = await stripeClient();
|
|
12129
|
+
const secret = getConfig().STRIPE_WEBHOOK_SECRET;
|
|
12130
|
+
if (!stripe || !secret || !signature) return null;
|
|
12131
|
+
return stripe.webhooks.constructEvent(raw, signature, secret);
|
|
12132
|
+
}
|
|
12133
|
+
function periodEnd(sub) {
|
|
12134
|
+
const item = sub.items?.data?.[0];
|
|
12135
|
+
const raw = item?.current_period_end ?? sub.current_period_end;
|
|
12136
|
+
return typeof raw === "number" ? new Date(raw * 1e3) : null;
|
|
12137
|
+
}
|
|
12138
|
+
async function handleEvent(db, event) {
|
|
12139
|
+
if (!HANDLED.has(event.type)) return `ignored ${event.type}`;
|
|
12140
|
+
const stripe = await stripeClient();
|
|
12141
|
+
if (!stripe) return "billing disabled";
|
|
12142
|
+
let subscription = null;
|
|
12143
|
+
if (event.type === "checkout.session.completed") {
|
|
12144
|
+
const session = event.data.object;
|
|
12145
|
+
const id = typeof session.subscription === "string" ? session.subscription : session.subscription?.id;
|
|
12146
|
+
if (!id) return "checkout completed with no subscription";
|
|
12147
|
+
subscription = await stripe.subscriptions.retrieve(id);
|
|
12148
|
+
} else {
|
|
12149
|
+
subscription = event.data.object;
|
|
12150
|
+
}
|
|
12151
|
+
const customerId = typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id;
|
|
12152
|
+
if (!customerId) return "event carried no customer";
|
|
12153
|
+
const priceId = subscription.items?.data?.[0]?.price?.id ?? null;
|
|
12154
|
+
const tier = event.type === "customer.subscription.deleted" ? "free" : tierForPrice(priceId, subscription.metadata?.tier);
|
|
12155
|
+
const applied = await applySubscriptionEvent(db, customerId, {
|
|
12156
|
+
tier,
|
|
12157
|
+
status: event.type === "customer.subscription.deleted" ? "canceled" : subscription.status,
|
|
12158
|
+
stripeSubscriptionId: subscription.id,
|
|
12159
|
+
currentPeriodEnd: periodEnd(subscription),
|
|
12160
|
+
cancelAtPeriodEnd: Boolean(subscription.cancel_at_period_end),
|
|
12161
|
+
eventAt: new Date(event.created * 1e3)
|
|
12162
|
+
});
|
|
12163
|
+
return applied ? `${event.type}: customer ${customerId} \u2192 ${tier} (${subscription.status})` : `${event.type}: superseded by a newer event, dropped`;
|
|
12164
|
+
}
|
|
12165
|
+
var API_VERSION, clientPromise2, HANDLED;
|
|
12166
|
+
var init_stripe = __esm({
|
|
12167
|
+
"src/billing/stripe.ts"() {
|
|
12168
|
+
"use strict";
|
|
12169
|
+
init_esm_shims();
|
|
12170
|
+
init_config();
|
|
12171
|
+
init_logger();
|
|
12172
|
+
init_plans();
|
|
12173
|
+
init_subscriptions();
|
|
12174
|
+
API_VERSION = "2025-03-31.basil";
|
|
12175
|
+
clientPromise2 = null;
|
|
12176
|
+
HANDLED = /* @__PURE__ */ new Set([
|
|
12177
|
+
"checkout.session.completed",
|
|
12178
|
+
"customer.subscription.created",
|
|
12179
|
+
"customer.subscription.updated",
|
|
12180
|
+
"customer.subscription.deleted"
|
|
12181
|
+
]);
|
|
12182
|
+
}
|
|
12183
|
+
});
|
|
12184
|
+
|
|
10623
12185
|
// src/github/hops.ts
|
|
10624
|
-
import
|
|
12186
|
+
import semver7 from "semver";
|
|
10625
12187
|
function planHops(versions, from, to) {
|
|
10626
|
-
if (!
|
|
10627
|
-
if (!
|
|
10628
|
-
const fromMajor =
|
|
10629
|
-
const toMajor =
|
|
12188
|
+
if (!semver7.valid(from) || !semver7.valid(to)) return [];
|
|
12189
|
+
if (!semver7.gt(to, from)) return [];
|
|
12190
|
+
const fromMajor = semver7.major(from);
|
|
12191
|
+
const toMajor = semver7.major(to);
|
|
10630
12192
|
if (toMajor - fromMajor < 2) return [];
|
|
10631
12193
|
const stops = [];
|
|
10632
12194
|
for (let major = fromMajor + 1; major < toMajor; major++) {
|
|
10633
|
-
const best =
|
|
12195
|
+
const best = semver7.maxSatisfying(versions, `${major}.x`, { includePrerelease: false });
|
|
10634
12196
|
if (best) stops.push(best);
|
|
10635
12197
|
}
|
|
10636
12198
|
const path2 = [from, ...stops, to];
|
|
@@ -10644,8 +12206,8 @@ function hopPairs(path2) {
|
|
|
10644
12206
|
return pairs;
|
|
10645
12207
|
}
|
|
10646
12208
|
function tooFarToSequence(from, to) {
|
|
10647
|
-
if (!
|
|
10648
|
-
return
|
|
12209
|
+
if (!semver7.valid(from) || !semver7.valid(to)) return false;
|
|
12210
|
+
return semver7.major(to) - semver7.major(from) > MAX_HOPS2;
|
|
10649
12211
|
}
|
|
10650
12212
|
var MAX_HOPS2;
|
|
10651
12213
|
var init_hops = __esm({
|
|
@@ -10663,8 +12225,8 @@ function briefable(dep) {
|
|
|
10663
12225
|
function rank(dep) {
|
|
10664
12226
|
return dep.advisories * 1e3 + dep.majorsBehind * 100 + (dep.deprecated ? 50 : 0);
|
|
10665
12227
|
}
|
|
10666
|
-
function verdictOf(removed, arityChanged,
|
|
10667
|
-
if (
|
|
12228
|
+
function verdictOf(removed, arityChanged, inconclusive2) {
|
|
12229
|
+
if (inconclusive2) return "unknown";
|
|
10668
12230
|
if (removed.length > 0) return "removes-exports";
|
|
10669
12231
|
if (arityChanged.length > 0) return "arity-changed";
|
|
10670
12232
|
return "clean";
|
|
@@ -10685,17 +12247,17 @@ async function briefUpgrade(db, dep, knownVersions = []) {
|
|
|
10685
12247
|
toVersion: base.toVersion
|
|
10686
12248
|
});
|
|
10687
12249
|
const removed = diff.removed.map((s) => s.path);
|
|
10688
|
-
const
|
|
12250
|
+
const inconclusive2 = "inconclusive" in diff ? diff.inconclusive : void 0;
|
|
10689
12251
|
const { hops, sequenceNote } = await planSequence(db, dep, base, knownVersions);
|
|
10690
12252
|
return {
|
|
10691
12253
|
...base,
|
|
10692
|
-
verdict: verdictOf(removed, diff.arityChanged,
|
|
12254
|
+
verdict: verdictOf(removed, diff.arityChanged, inconclusive2),
|
|
10693
12255
|
removed,
|
|
10694
12256
|
arityChanged: diff.arityChanged,
|
|
10695
12257
|
typeOnlyRemoved: diff.typeOnlyRemoved,
|
|
10696
12258
|
newlyDeprecated: diff.deprecated ?? [],
|
|
10697
12259
|
hops,
|
|
10698
|
-
...
|
|
12260
|
+
...inconclusive2 ? { inconclusive: inconclusive2 } : {},
|
|
10699
12261
|
...sequenceNote ? { sequenceNote } : {}
|
|
10700
12262
|
};
|
|
10701
12263
|
}
|
|
@@ -10767,6 +12329,28 @@ var init_brief = __esm({
|
|
|
10767
12329
|
}
|
|
10768
12330
|
});
|
|
10769
12331
|
|
|
12332
|
+
// src/db/askSpend.ts
|
|
12333
|
+
import { and as and20, eq as eq23, sql as sql14 } from "drizzle-orm";
|
|
12334
|
+
async function getAskSpendToday(db, ownerId) {
|
|
12335
|
+
const rows = await db.select({ usdMicros: askSpendDaily.usdMicros }).from(askSpendDaily).where(and20(eq23(askSpendDaily.ownerId, ownerId), eq23(askSpendDaily.date, sql14`CURRENT_DATE`))).limit(1);
|
|
12336
|
+
return rows[0]?.usdMicros ?? 0;
|
|
12337
|
+
}
|
|
12338
|
+
async function addAskSpend(db, ownerId, micros) {
|
|
12339
|
+
if (micros === 0) return getAskSpendToday(db, ownerId);
|
|
12340
|
+
const rows = await db.insert(askSpendDaily).values({ ownerId, date: sql14`CURRENT_DATE`, usdMicros: Math.max(0, micros) }).onConflictDoUpdate({
|
|
12341
|
+
target: [askSpendDaily.ownerId, askSpendDaily.date],
|
|
12342
|
+
set: { usdMicros: sql14`greatest(0, ${askSpendDaily.usdMicros} + ${micros})` }
|
|
12343
|
+
}).returning({ usdMicros: askSpendDaily.usdMicros });
|
|
12344
|
+
return rows[0]?.usdMicros ?? Math.max(0, micros);
|
|
12345
|
+
}
|
|
12346
|
+
var init_askSpend = __esm({
|
|
12347
|
+
"src/db/askSpend.ts"() {
|
|
12348
|
+
"use strict";
|
|
12349
|
+
init_esm_shims();
|
|
12350
|
+
init_schema();
|
|
12351
|
+
}
|
|
12352
|
+
});
|
|
12353
|
+
|
|
10770
12354
|
// src/github/scope.ts
|
|
10771
12355
|
function isUnassessed(u) {
|
|
10772
12356
|
return u.verdict === "unknown";
|
|
@@ -10905,7 +12489,7 @@ var init_runs = __esm({
|
|
|
10905
12489
|
});
|
|
10906
12490
|
|
|
10907
12491
|
// src/db/upgradeRuns.ts
|
|
10908
|
-
import { and as
|
|
12492
|
+
import { and as and21, desc as desc9, eq as eq24, gte as gte4, sql as sql15 } from "drizzle-orm";
|
|
10909
12493
|
async function recordUpgradeRuns(db, rows) {
|
|
10910
12494
|
if (rows.length === 0) return 0;
|
|
10911
12495
|
await db.insert(upgradeRuns).values(rows).onConflictDoUpdate({
|
|
@@ -10917,36 +12501,36 @@ async function recordUpgradeRuns(db, rows) {
|
|
|
10917
12501
|
upgradeRuns.runUrl
|
|
10918
12502
|
],
|
|
10919
12503
|
set: {
|
|
10920
|
-
severity:
|
|
10921
|
-
status:
|
|
10922
|
-
symbolsAffected:
|
|
10923
|
-
callSites:
|
|
10924
|
-
callSiteFiles:
|
|
10925
|
-
filesChanged:
|
|
10926
|
-
testsPassed:
|
|
10927
|
-
prUrl:
|
|
10928
|
-
repoId:
|
|
12504
|
+
severity: sql15`excluded.severity`,
|
|
12505
|
+
status: sql15`excluded.status`,
|
|
12506
|
+
symbolsAffected: sql15`excluded.symbols_affected`,
|
|
12507
|
+
callSites: sql15`excluded.call_sites`,
|
|
12508
|
+
callSiteFiles: sql15`excluded.call_site_files`,
|
|
12509
|
+
filesChanged: sql15`excluded.files_changed`,
|
|
12510
|
+
testsPassed: sql15`excluded.tests_passed`,
|
|
12511
|
+
prUrl: sql15`excluded.pr_url`,
|
|
12512
|
+
repoId: sql15`excluded.repo_id`
|
|
10929
12513
|
}
|
|
10930
12514
|
});
|
|
10931
12515
|
return rows.length;
|
|
10932
12516
|
}
|
|
10933
12517
|
async function findRepoIdByFullName(db, ownerId, fullName) {
|
|
10934
|
-
const rows = await db.select({ id: repos.id }).from(repos).where(
|
|
12518
|
+
const rows = await db.select({ id: repos.id }).from(repos).where(and21(eq24(repos.ownerId, ownerId), eq24(repos.fullName, fullName))).limit(1);
|
|
10935
12519
|
return rows[0]?.id ?? null;
|
|
10936
12520
|
}
|
|
10937
12521
|
async function listRunsForRepo(db, ownerId, repoId, limit = 50) {
|
|
10938
|
-
return db.select().from(upgradeRuns).where(
|
|
12522
|
+
return db.select().from(upgradeRuns).where(and21(eq24(upgradeRuns.ownerId, ownerId), eq24(upgradeRuns.repoId, repoId))).orderBy(desc9(upgradeRuns.createdAt)).limit(limit);
|
|
10939
12523
|
}
|
|
10940
12524
|
async function getUpgradeImpact(db, ownerId, days = 30) {
|
|
10941
12525
|
const since = new Date(Date.now() - days * 864e5);
|
|
10942
12526
|
const rows = await db.select({
|
|
10943
|
-
analysed:
|
|
10944
|
-
blocking:
|
|
10945
|
-
callSites:
|
|
10946
|
-
prsOpened:
|
|
10947
|
-
merged:
|
|
10948
|
-
unverified:
|
|
10949
|
-
}).from(upgradeRuns).where(
|
|
12527
|
+
analysed: sql15`count(*)::int`,
|
|
12528
|
+
blocking: sql15`count(*) filter (where ${upgradeRuns.severity} = 'blocking')::int`,
|
|
12529
|
+
callSites: sql15`coalesce(sum(${upgradeRuns.callSites}) filter (where ${upgradeRuns.severity} = 'blocking'), 0)::int`,
|
|
12530
|
+
prsOpened: sql15`count(*) filter (where ${upgradeRuns.status} in ('pr_open','merged'))::int`,
|
|
12531
|
+
merged: sql15`count(*) filter (where ${upgradeRuns.status} = 'merged')::int`,
|
|
12532
|
+
unverified: sql15`count(*) filter (where ${upgradeRuns.severity} = 'unverified')::int`
|
|
12533
|
+
}).from(upgradeRuns).where(and21(eq24(upgradeRuns.ownerId, ownerId), gte4(upgradeRuns.createdAt, since)));
|
|
10950
12534
|
return rows[0] ?? {
|
|
10951
12535
|
analysed: 0,
|
|
10952
12536
|
blocking: 0,
|
|
@@ -10966,6 +12550,59 @@ var init_upgradeRuns = __esm({
|
|
|
10966
12550
|
}
|
|
10967
12551
|
});
|
|
10968
12552
|
|
|
12553
|
+
// src/github/webhook.ts
|
|
12554
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
12555
|
+
function verifyWebhookSignature(secret, body, header) {
|
|
12556
|
+
if (!header?.startsWith("sha256=")) return false;
|
|
12557
|
+
const presented = Buffer.from(header.slice(7), "hex");
|
|
12558
|
+
const expected = createHmac("sha256", secret).update(body).digest();
|
|
12559
|
+
return presented.length === expected.length && timingSafeEqual(presented, expected);
|
|
12560
|
+
}
|
|
12561
|
+
function fullNames(value) {
|
|
12562
|
+
if (!Array.isArray(value)) return [];
|
|
12563
|
+
return value.map(
|
|
12564
|
+
(entry) => entry && typeof entry === "object" && typeof entry.full_name === "string" ? entry.full_name : null
|
|
12565
|
+
).filter((name) => name !== null);
|
|
12566
|
+
}
|
|
12567
|
+
function installationId(payload) {
|
|
12568
|
+
const install = payload.installation;
|
|
12569
|
+
const id = install && typeof install === "object" ? install.id : null;
|
|
12570
|
+
return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
|
|
12571
|
+
}
|
|
12572
|
+
function parseWebhook(event, payload) {
|
|
12573
|
+
if (!payload || typeof payload !== "object") return { kind: "ignored", reason: "no payload" };
|
|
12574
|
+
const body = payload;
|
|
12575
|
+
const id = installationId(body);
|
|
12576
|
+
if (id === null) return { kind: "ignored", reason: "no installation id" };
|
|
12577
|
+
const action = typeof body.action === "string" ? body.action : "";
|
|
12578
|
+
if (event === "installation_repositories") {
|
|
12579
|
+
const added = fullNames(body.repositories_added);
|
|
12580
|
+
const removed = fullNames(body.repositories_removed);
|
|
12581
|
+
if (added.length === 0 && removed.length === 0) {
|
|
12582
|
+
return { kind: "ignored", reason: "no repository changes" };
|
|
12583
|
+
}
|
|
12584
|
+
return { kind: "repos-changed", installationId: id, added, removed };
|
|
12585
|
+
}
|
|
12586
|
+
if (event === "installation") {
|
|
12587
|
+
if (action === "deleted") return { kind: "uninstalled", installationId: id };
|
|
12588
|
+
if (action === "added" || action === "removed") {
|
|
12589
|
+
const added = fullNames(body.repositories_added);
|
|
12590
|
+
const removed = fullNames(body.repositories_removed);
|
|
12591
|
+
if (added.length || removed.length) {
|
|
12592
|
+
return { kind: "repos-changed", installationId: id, added, removed };
|
|
12593
|
+
}
|
|
12594
|
+
}
|
|
12595
|
+
return { kind: "ignored", reason: `installation.${action || "unknown"}` };
|
|
12596
|
+
}
|
|
12597
|
+
return { kind: "ignored", reason: `event ${event ?? "unknown"}` };
|
|
12598
|
+
}
|
|
12599
|
+
var init_webhook = __esm({
|
|
12600
|
+
"src/github/webhook.ts"() {
|
|
12601
|
+
"use strict";
|
|
12602
|
+
init_esm_shims();
|
|
12603
|
+
}
|
|
12604
|
+
});
|
|
12605
|
+
|
|
10969
12606
|
// src/github/sbom.ts
|
|
10970
12607
|
function parseNpmPurl(purl) {
|
|
10971
12608
|
if (!purl.startsWith("pkg:npm/")) return null;
|
|
@@ -11007,10 +12644,10 @@ function parseRelationships(json2) {
|
|
|
11007
12644
|
}
|
|
11008
12645
|
return out;
|
|
11009
12646
|
}
|
|
11010
|
-
async function fetchResolvedTree(
|
|
12647
|
+
async function fetchResolvedTree(installationId2, fullName) {
|
|
11011
12648
|
try {
|
|
11012
12649
|
const data = await installationGet(
|
|
11013
|
-
|
|
12650
|
+
installationId2,
|
|
11014
12651
|
`/repos/${fullName}/dependency-graph/sbom`
|
|
11015
12652
|
);
|
|
11016
12653
|
const deps = parseSbom(data);
|
|
@@ -11048,7 +12685,7 @@ async function scanRepo(db, repo) {
|
|
|
11048
12685
|
branch
|
|
11049
12686
|
);
|
|
11050
12687
|
const resolvedTree = await fetchResolvedTree(repo.installationId, repo.fullName);
|
|
11051
|
-
const drift = await computeDrift(db, manifests, resolvedTree);
|
|
12688
|
+
const drift = await computeDrift(db, manifests, resolvedTree, repo.ownerId);
|
|
11052
12689
|
await saveScan(db, repo.id, { manifests, drift, installCommand: installCommand2 });
|
|
11053
12690
|
return {
|
|
11054
12691
|
...base,
|
|
@@ -11074,15 +12711,24 @@ async function scanRepo(db, repo) {
|
|
|
11074
12711
|
};
|
|
11075
12712
|
}
|
|
11076
12713
|
}
|
|
11077
|
-
async function
|
|
12714
|
+
async function scanRepos(db, rows) {
|
|
11078
12715
|
const results = [];
|
|
11079
12716
|
for (const repo of rows) {
|
|
11080
12717
|
results.push(await scanRepo(db, repo));
|
|
11081
12718
|
}
|
|
11082
12719
|
return results;
|
|
11083
12720
|
}
|
|
11084
|
-
|
|
11085
|
-
|
|
12721
|
+
function byRecentPush(rows, inventory) {
|
|
12722
|
+
const pushedAt = (repo) => {
|
|
12723
|
+
const parsed = Date.parse(repo.pushedAt ?? "");
|
|
12724
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
12725
|
+
};
|
|
12726
|
+
const rank2 = new Map(
|
|
12727
|
+
[...inventory].sort((a, b) => pushedAt(b) - pushedAt(a)).map((repo, index2) => [repo.fullName, index2])
|
|
12728
|
+
);
|
|
12729
|
+
return [...rows].sort(
|
|
12730
|
+
(a, b) => (rank2.get(a.fullName) ?? Number.MAX_SAFE_INTEGER) - (rank2.get(b.fullName) ?? Number.MAX_SAFE_INTEGER)
|
|
12731
|
+
);
|
|
11086
12732
|
}
|
|
11087
12733
|
var init_repoScan = __esm({
|
|
11088
12734
|
"src/pipeline/repoScan.ts"() {
|
|
@@ -11100,16 +12746,27 @@ var init_repoScan = __esm({
|
|
|
11100
12746
|
// src/mcp/http.ts
|
|
11101
12747
|
var http_exports = {};
|
|
11102
12748
|
__export(http_exports, {
|
|
12749
|
+
errorEnvelope: () => errorEnvelope,
|
|
11103
12750
|
secretEquals: () => secretEquals,
|
|
11104
12751
|
startHttpServer: () => startHttpServer
|
|
11105
12752
|
});
|
|
11106
|
-
import { createHash as createHash9, timingSafeEqual } from "crypto";
|
|
12753
|
+
import { createHash as createHash9, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
11107
12754
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11108
12755
|
function rpcError(code, message) {
|
|
11109
12756
|
return { jsonrpc: "2.0", error: { code, message }, id: null };
|
|
11110
12757
|
}
|
|
12758
|
+
function errorEnvelope(err, path2) {
|
|
12759
|
+
const clientFault = typeof err.type === "string" && err.type.startsWith("entity.");
|
|
12760
|
+
const status = typeof err.status === "number" && err.status >= 400 && err.status < 600 ? err.status : 500;
|
|
12761
|
+
const message = clientFault ? err.type === "entity.too.large" ? "Request body too large (limit 1mb)." : "Request body is not valid JSON." : "Internal error.";
|
|
12762
|
+
const body = path2 === "/mcp" ? (
|
|
12763
|
+
// -32700 is JSON-RPC's own "parse error"; -32603 is "internal error".
|
|
12764
|
+
rpcError(clientFault ? -32700 : -32603, message)
|
|
12765
|
+
) : { error: message };
|
|
12766
|
+
return { status, body, clientFault };
|
|
12767
|
+
}
|
|
11111
12768
|
function secretEquals(a, b) {
|
|
11112
|
-
return
|
|
12769
|
+
return timingSafeEqual2(
|
|
11113
12770
|
createHash9("sha256").update(a).digest(),
|
|
11114
12771
|
createHash9("sha256").update(b).digest()
|
|
11115
12772
|
);
|
|
@@ -11118,7 +12775,7 @@ async function startHttpServer(opts = {}) {
|
|
|
11118
12775
|
const config = getConfig();
|
|
11119
12776
|
const port = opts.port ?? config.PORT;
|
|
11120
12777
|
const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
|
|
11121
|
-
const { db } = createDb({ max: 20 });
|
|
12778
|
+
const { db, close: closeDb } = createDb({ max: 20 });
|
|
11122
12779
|
if (!process.env.REDIS_URL) {
|
|
11123
12780
|
logger.warn(
|
|
11124
12781
|
"REDIS_URL not set, response caching is OFF; every request recomputes on the database. Set REDIS_URL before serving real traffic (and it also backs the rate limiter across instances)."
|
|
@@ -11140,7 +12797,14 @@ async function startHttpServer(opts = {}) {
|
|
|
11140
12797
|
const app = express();
|
|
11141
12798
|
app.set("trust proxy", 1);
|
|
11142
12799
|
app.use(helmet());
|
|
11143
|
-
app.use(
|
|
12800
|
+
app.use(
|
|
12801
|
+
express.json({
|
|
12802
|
+
limit: "1mb",
|
|
12803
|
+
verify: (req, _res, buf) => {
|
|
12804
|
+
req.rawBody = buf;
|
|
12805
|
+
}
|
|
12806
|
+
})
|
|
12807
|
+
);
|
|
11144
12808
|
app.get("/healthz", (_req, res) => {
|
|
11145
12809
|
res.status(200).json({ status: "ok" });
|
|
11146
12810
|
});
|
|
@@ -11152,7 +12816,7 @@ async function startHttpServer(opts = {}) {
|
|
|
11152
12816
|
}
|
|
11153
12817
|
const header = req.headers.authorization;
|
|
11154
12818
|
const presented = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
11155
|
-
if (presented
|
|
12819
|
+
if (!secretEquals(presented, token)) {
|
|
11156
12820
|
res.status(401).end();
|
|
11157
12821
|
return;
|
|
11158
12822
|
}
|
|
@@ -11166,6 +12830,11 @@ async function startHttpServer(opts = {}) {
|
|
|
11166
12830
|
...makeStore ? { store: makeStore("rl:ip:") } : {},
|
|
11167
12831
|
message: rpcError(-32029, "Rate limit exceeded.")
|
|
11168
12832
|
});
|
|
12833
|
+
app.get("/capabilities", ipLimiter, (req, res) => {
|
|
12834
|
+
const q = typeof req.query.q === "string" ? req.query.q : "";
|
|
12835
|
+
const limit = Math.min(Number(req.query.limit) || 6, CAPABILITIES.length);
|
|
12836
|
+
res.json({ capabilities: q ? searchCapabilities(q, limit) : CAPABILITIES });
|
|
12837
|
+
});
|
|
11169
12838
|
const auth = async (req, res, next) => {
|
|
11170
12839
|
const header = req.headers.authorization;
|
|
11171
12840
|
const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
@@ -11186,9 +12855,30 @@ async function startHttpServer(opts = {}) {
|
|
|
11186
12855
|
res.status(500).json(rpcError(-32603, "Internal error."));
|
|
11187
12856
|
}
|
|
11188
12857
|
};
|
|
12858
|
+
const entitlementCache = /* @__PURE__ */ new Map();
|
|
12859
|
+
const ENTITLEMENT_TTL_MS = 6e4;
|
|
12860
|
+
const invalidateEntitlement = (ownerId) => entitlementCache.delete(ownerId);
|
|
12861
|
+
const resolveEntitlement = async (ownerId) => {
|
|
12862
|
+
if (!ownerId) return entitlementFor(db, null);
|
|
12863
|
+
const hit = entitlementCache.get(ownerId);
|
|
12864
|
+
if (hit && Date.now() - hit.at < ENTITLEMENT_TTL_MS) return hit.value;
|
|
12865
|
+
const value = await entitlementFor(db, ownerId);
|
|
12866
|
+
entitlementCache.set(ownerId, { at: Date.now(), value });
|
|
12867
|
+
return value;
|
|
12868
|
+
};
|
|
11189
12869
|
const keyLimiter = rateLimit({
|
|
11190
12870
|
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
11191
|
-
|
|
12871
|
+
// The burst ceiling is the plan's, not one number for everybody. Read from
|
|
12872
|
+
// the cached entitlement when `quota` has already resolved it on a previous
|
|
12873
|
+
// request for this key, and otherwise from LURQ_RATE_LIMIT_MAX — the limiter
|
|
12874
|
+
// runs BEFORE `quota` in the chain, so the first request of a minute is
|
|
12875
|
+
// metered at the configured default and the rest at the plan's rate. Paying
|
|
12876
|
+
// for Enterprise should not leave you sharing the free tier's burst.
|
|
12877
|
+
limit: (req) => {
|
|
12878
|
+
const ownerId = req.lurqKey?.ownerId;
|
|
12879
|
+
const cached3 = ownerId ? entitlementCache.get(ownerId) : void 0;
|
|
12880
|
+
return cached3?.value.plan.ratePerMinute ?? config.LURQ_RATE_LIMIT_MAX;
|
|
12881
|
+
},
|
|
11192
12882
|
standardHeaders: "draft-7",
|
|
11193
12883
|
legacyHeaders: false,
|
|
11194
12884
|
// Key on the resolved API key's unique row id (always present — auth runs
|
|
@@ -11203,6 +12893,30 @@ async function startHttpServer(opts = {}) {
|
|
|
11203
12893
|
...makeStore ? { store: makeStore("rl:key:") } : {},
|
|
11204
12894
|
message: rpcError(-32029, "Rate limit exceeded.")
|
|
11205
12895
|
});
|
|
12896
|
+
const quota = async (req, res, next) => {
|
|
12897
|
+
const authed = req;
|
|
12898
|
+
const ownerId = authed.lurqKey?.ownerId ?? null;
|
|
12899
|
+
try {
|
|
12900
|
+
const ent = await resolveEntitlement(ownerId);
|
|
12901
|
+
authed.entitlement = ent;
|
|
12902
|
+
if (!ent.withinQuota) {
|
|
12903
|
+
res.status(402).json(
|
|
12904
|
+
rpcError(
|
|
12905
|
+
-32002,
|
|
12906
|
+
`Monthly limit reached for the ${ent.plan.name} plan (${ent.used}/${ent.plan.monthlyCalls} calls). It resets when the month turns. Upgrade at ${config.LURQ_WEB_URL}/#pricing`
|
|
12907
|
+
)
|
|
12908
|
+
);
|
|
12909
|
+
return;
|
|
12910
|
+
}
|
|
12911
|
+
next();
|
|
12912
|
+
} catch (err) {
|
|
12913
|
+
logger.error(
|
|
12914
|
+
"quota lookup failed, serving anyway:",
|
|
12915
|
+
err instanceof Error ? err.message : String(err)
|
|
12916
|
+
);
|
|
12917
|
+
next();
|
|
12918
|
+
}
|
|
12919
|
+
};
|
|
11206
12920
|
const requireIssuerSecret = (req, res, next) => {
|
|
11207
12921
|
const secret = config.LURQ_ISSUER_SECRET;
|
|
11208
12922
|
if (!secret) {
|
|
@@ -11226,6 +12940,154 @@ async function startHttpServer(opts = {}) {
|
|
|
11226
12940
|
lastUsedAt: row.lastUsedAt,
|
|
11227
12941
|
revokedAt: row.revokedAt
|
|
11228
12942
|
});
|
|
12943
|
+
app.post("/billing/checkout", requireIssuerSecret, async (req, res) => {
|
|
12944
|
+
if (!billingEnabled()) {
|
|
12945
|
+
res.status(404).end();
|
|
12946
|
+
return;
|
|
12947
|
+
}
|
|
12948
|
+
const ownerId = typeof req.body?.ownerId === "string" ? req.body.ownerId.trim() : "";
|
|
12949
|
+
const tier = typeof req.body?.tier === "string" ? req.body.tier : "pro";
|
|
12950
|
+
const email = typeof req.body?.email === "string" ? req.body.email : null;
|
|
12951
|
+
if (!ownerId) {
|
|
12952
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
12953
|
+
return;
|
|
12954
|
+
}
|
|
12955
|
+
if (!(tier in PLANS) || !PLANS[tier].paid) {
|
|
12956
|
+
res.status(400).json({ error: `${tier} is not a purchasable plan.` });
|
|
12957
|
+
return;
|
|
12958
|
+
}
|
|
12959
|
+
try {
|
|
12960
|
+
const url = await createCheckoutSession(db, { ownerId, tier, email });
|
|
12961
|
+
if (!url) {
|
|
12962
|
+
res.status(503).json({ error: "That plan is not available for checkout yet." });
|
|
12963
|
+
return;
|
|
12964
|
+
}
|
|
12965
|
+
res.status(200).json({ url });
|
|
12966
|
+
} catch (err) {
|
|
12967
|
+
logger.error("checkout failed:", err instanceof Error ? err.message : String(err));
|
|
12968
|
+
res.status(502).json({ error: "Could not start checkout." });
|
|
12969
|
+
}
|
|
12970
|
+
});
|
|
12971
|
+
app.post("/billing/portal", requireIssuerSecret, async (req, res) => {
|
|
12972
|
+
if (!billingEnabled()) {
|
|
12973
|
+
res.status(404).end();
|
|
12974
|
+
return;
|
|
12975
|
+
}
|
|
12976
|
+
const ownerId = typeof req.body?.ownerId === "string" ? req.body.ownerId.trim() : "";
|
|
12977
|
+
if (!ownerId) {
|
|
12978
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
12979
|
+
return;
|
|
12980
|
+
}
|
|
12981
|
+
try {
|
|
12982
|
+
const url = await createPortalSession(db, ownerId);
|
|
12983
|
+
if (!url) {
|
|
12984
|
+
res.status(404).json({ error: "No billing account for this user yet." });
|
|
12985
|
+
return;
|
|
12986
|
+
}
|
|
12987
|
+
res.status(200).json({ url });
|
|
12988
|
+
} catch (err) {
|
|
12989
|
+
logger.error("portal failed:", err instanceof Error ? err.message : String(err));
|
|
12990
|
+
res.status(502).json({ error: "Could not open the billing portal." });
|
|
12991
|
+
}
|
|
12992
|
+
});
|
|
12993
|
+
const ASK_DAILY_LIMIT_MICROS = Math.max(
|
|
12994
|
+
0,
|
|
12995
|
+
Math.round(Number(process.env.LURQ_ASK_DAILY_USD ?? "3") * 1e6)
|
|
12996
|
+
);
|
|
12997
|
+
app.get("/ask-budget", requireIssuerSecret, async (req, res) => {
|
|
12998
|
+
const ownerId = typeof req.query.ownerId === "string" ? req.query.ownerId.trim() : "";
|
|
12999
|
+
if (!ownerId) {
|
|
13000
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
13001
|
+
return;
|
|
13002
|
+
}
|
|
13003
|
+
try {
|
|
13004
|
+
const spentMicros = await getAskSpendToday(db, ownerId);
|
|
13005
|
+
res.status(200).json({ spentMicros, limitMicros: ASK_DAILY_LIMIT_MICROS });
|
|
13006
|
+
} catch (err) {
|
|
13007
|
+
logger.error("ask budget read failed:", err instanceof Error ? err.message : String(err));
|
|
13008
|
+
res.status(500).json({ error: "Could not read the Ask budget." });
|
|
13009
|
+
}
|
|
13010
|
+
});
|
|
13011
|
+
app.post("/ask-budget", requireIssuerSecret, async (req, res) => {
|
|
13012
|
+
const body = req.body ?? {};
|
|
13013
|
+
const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : "";
|
|
13014
|
+
const micros = Number(body.usdMicros);
|
|
13015
|
+
if (!ownerId) {
|
|
13016
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
13017
|
+
return;
|
|
13018
|
+
}
|
|
13019
|
+
if (!Number.isFinite(micros) || Math.abs(micros) > ASK_DAILY_LIMIT_MICROS) {
|
|
13020
|
+
res.status(400).json({ error: "usdMicros must be a finite delta within one day's limit." });
|
|
13021
|
+
return;
|
|
13022
|
+
}
|
|
13023
|
+
try {
|
|
13024
|
+
const spentMicros = await addAskSpend(db, ownerId, Math.round(micros));
|
|
13025
|
+
res.status(200).json({ spentMicros, limitMicros: ASK_DAILY_LIMIT_MICROS });
|
|
13026
|
+
} catch (err) {
|
|
13027
|
+
logger.error("ask budget write failed:", err instanceof Error ? err.message : String(err));
|
|
13028
|
+
res.status(500).json({ error: "Could not record Ask spend." });
|
|
13029
|
+
}
|
|
13030
|
+
});
|
|
13031
|
+
app.get("/billing/subscription", requireIssuerSecret, async (req, res) => {
|
|
13032
|
+
const ownerId = typeof req.query.ownerId === "string" ? req.query.ownerId.trim() : "";
|
|
13033
|
+
if (!ownerId) {
|
|
13034
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
13035
|
+
return;
|
|
13036
|
+
}
|
|
13037
|
+
try {
|
|
13038
|
+
const [sub, ent] = await Promise.all([
|
|
13039
|
+
getSubscription(db, ownerId),
|
|
13040
|
+
entitlementFor(db, ownerId)
|
|
13041
|
+
]);
|
|
13042
|
+
res.status(200).json({
|
|
13043
|
+
tier: ent.plan.tier,
|
|
13044
|
+
planName: ent.plan.name,
|
|
13045
|
+
status: sub?.status ?? null,
|
|
13046
|
+
currentPeriodEnd: sub?.currentPeriodEnd?.toISOString() ?? null,
|
|
13047
|
+
cancelAtPeriodEnd: sub?.cancelAtPeriodEnd ?? false,
|
|
13048
|
+
used: ent.used,
|
|
13049
|
+
limit: ent.plan.monthlyCalls,
|
|
13050
|
+
billingEnabled: billingEnabled(),
|
|
13051
|
+
manageable: Boolean(sub?.stripeCustomerId)
|
|
13052
|
+
});
|
|
13053
|
+
} catch (err) {
|
|
13054
|
+
logger.error("subscription read failed:", err instanceof Error ? err.message : String(err));
|
|
13055
|
+
res.status(500).json({ error: "Could not read the subscription." });
|
|
13056
|
+
}
|
|
13057
|
+
});
|
|
13058
|
+
app.post("/billing/webhook", async (req, res) => {
|
|
13059
|
+
const raw = req.rawBody;
|
|
13060
|
+
const signature = req.headers["stripe-signature"];
|
|
13061
|
+
let event;
|
|
13062
|
+
try {
|
|
13063
|
+
event = await constructEvent(
|
|
13064
|
+
raw ?? "",
|
|
13065
|
+
typeof signature === "string" ? signature : void 0
|
|
13066
|
+
);
|
|
13067
|
+
} catch (err) {
|
|
13068
|
+
logger.warn(
|
|
13069
|
+
`billing webhook: bad signature: ${err instanceof Error ? err.message : String(err)}`
|
|
13070
|
+
);
|
|
13071
|
+
res.status(400).json({ error: "Invalid signature." });
|
|
13072
|
+
return;
|
|
13073
|
+
}
|
|
13074
|
+
if (!event) {
|
|
13075
|
+
res.status(404).end();
|
|
13076
|
+
return;
|
|
13077
|
+
}
|
|
13078
|
+
res.status(200).json({ received: true });
|
|
13079
|
+
try {
|
|
13080
|
+
const outcome = await handleEvent(db, event);
|
|
13081
|
+
logger.info(`billing webhook: ${outcome}`);
|
|
13082
|
+
const object = event.data.object;
|
|
13083
|
+
if (typeof object.customer === "string") {
|
|
13084
|
+
const row = await getSubscriptionByCustomer(db, object.customer);
|
|
13085
|
+
if (row) invalidateEntitlement(row.ownerId);
|
|
13086
|
+
}
|
|
13087
|
+
} catch (err) {
|
|
13088
|
+
logger.error("billing webhook failed:", err instanceof Error ? err.message : String(err));
|
|
13089
|
+
}
|
|
13090
|
+
});
|
|
11229
13091
|
app.post("/keys", requireIssuerSecret, async (req, res) => {
|
|
11230
13092
|
const body = req.body ?? {};
|
|
11231
13093
|
const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : "";
|
|
@@ -11371,7 +13233,10 @@ async function startHttpServer(opts = {}) {
|
|
|
11371
13233
|
const offsetRaw = typeof req.query.offset === "string" ? Number(req.query.offset) : NaN;
|
|
11372
13234
|
const offset = Number.isInteger(offsetRaw) && offsetRaw > 0 ? offsetRaw : 0;
|
|
11373
13235
|
try {
|
|
11374
|
-
const { total, packages: rows } = await getContributionsByOwner(db, ownerId, {
|
|
13236
|
+
const { total, packages: rows } = await getContributionsByOwner(db, ownerId, {
|
|
13237
|
+
limit,
|
|
13238
|
+
offset
|
|
13239
|
+
});
|
|
11375
13240
|
res.status(200).json({ total, packages: rows });
|
|
11376
13241
|
} catch (err) {
|
|
11377
13242
|
logger.error("contributions read failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -11427,34 +13292,85 @@ async function startHttpServer(opts = {}) {
|
|
|
11427
13292
|
if (raw.scope !== "security" && raw.scope !== "blocking" && raw.scope !== "all") return null;
|
|
11428
13293
|
return { enabled: raw.enabled, scope: raw.scope, autoMerge: raw.autoMerge };
|
|
11429
13294
|
}
|
|
11430
|
-
|
|
11431
|
-
const
|
|
11432
|
-
const
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
13295
|
+
const syncInstallation = async (ownerId, installationId2, scanOnly) => {
|
|
13296
|
+
const found = await listInstallationRepos(installationId2);
|
|
13297
|
+
const connected = await upsertRepos(
|
|
13298
|
+
db,
|
|
13299
|
+
found.map((repo) => ({
|
|
13300
|
+
ownerId,
|
|
13301
|
+
installationId: installationId2,
|
|
13302
|
+
fullName: repo.fullName,
|
|
13303
|
+
defaultBranch: repo.defaultBranch,
|
|
13304
|
+
isPrivate: repo.isPrivate
|
|
13305
|
+
}))
|
|
13306
|
+
);
|
|
13307
|
+
void (async () => {
|
|
13308
|
+
const rows = byRecentPush(await listRepos(db, ownerId), found);
|
|
13309
|
+
const only = scanOnly ? new Set(scanOnly) : null;
|
|
13310
|
+
await scanRepos(db, only ? rows.filter((row) => only.has(row.fullName)) : rows);
|
|
13311
|
+
})().catch((err) => {
|
|
13312
|
+
logger.warn(`repo scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
13313
|
+
});
|
|
13314
|
+
return connected;
|
|
13315
|
+
};
|
|
13316
|
+
app.post(
|
|
13317
|
+
"/repos/connect",
|
|
13318
|
+
requireIssuerSecret,
|
|
13319
|
+
requireGithubApp,
|
|
13320
|
+
async (req, res) => {
|
|
13321
|
+
const ownerId = ownerFrom(req);
|
|
13322
|
+
const installationRaw = (req.body ?? {}).installationId;
|
|
13323
|
+
const installationId2 = Number(installationRaw);
|
|
13324
|
+
if (!ownerId || !Number.isInteger(installationId2) || installationId2 <= 0) {
|
|
13325
|
+
res.status(400).json({ error: "ownerId and installationId are required." });
|
|
13326
|
+
return;
|
|
13327
|
+
}
|
|
13328
|
+
try {
|
|
13329
|
+
res.status(200).json({ connected: await syncInstallation(ownerId, installationId2) });
|
|
13330
|
+
} catch (err) {
|
|
13331
|
+
const status = err instanceof GithubAppError ? err.status : 502;
|
|
13332
|
+
logger.error("repo connect failed:", err instanceof Error ? err.message : String(err));
|
|
13333
|
+
res.status(status).json({ error: "Could not read the GitHub installation." });
|
|
13334
|
+
}
|
|
13335
|
+
}
|
|
13336
|
+
);
|
|
13337
|
+
app.post("/github/webhook", requireGithubApp, async (req, res) => {
|
|
13338
|
+
const secret = config.LURQ_GITHUB_WEBHOOK_SECRET;
|
|
13339
|
+
if (!secret) {
|
|
13340
|
+
res.status(404).end();
|
|
11436
13341
|
return;
|
|
11437
13342
|
}
|
|
13343
|
+
const raw = req.rawBody;
|
|
13344
|
+
const signature = req.headers["x-hub-signature-256"];
|
|
13345
|
+
if (!raw || !verifyWebhookSignature(secret, raw, typeof signature === "string" ? signature : void 0)) {
|
|
13346
|
+
res.status(401).json({ error: "Invalid signature." });
|
|
13347
|
+
return;
|
|
13348
|
+
}
|
|
13349
|
+
const event = req.headers["x-github-event"];
|
|
13350
|
+
const action = parseWebhook(typeof event === "string" ? event : void 0, req.body);
|
|
13351
|
+
res.status(202).end();
|
|
13352
|
+
if (action.kind === "ignored") return;
|
|
11438
13353
|
try {
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
)
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
}
|
|
11453
|
-
|
|
13354
|
+
if (action.kind === "uninstalled") {
|
|
13355
|
+
const forgotten = await deleteReposByInstallation(db, action.installationId);
|
|
13356
|
+
logger.info(`installation ${action.installationId} uninstalled, forgot ${forgotten} repos`);
|
|
13357
|
+
return;
|
|
13358
|
+
}
|
|
13359
|
+
if (action.removed.length > 0) {
|
|
13360
|
+
await deleteReposByInstallation(db, action.installationId, action.removed);
|
|
13361
|
+
}
|
|
13362
|
+
if (action.added.length === 0) return;
|
|
13363
|
+
const ownerId = await ownerForInstallation(db, action.installationId);
|
|
13364
|
+
if (!ownerId) {
|
|
13365
|
+
logger.warn(`webhook for unlinked installation ${action.installationId}, ignored`);
|
|
13366
|
+
return;
|
|
13367
|
+
}
|
|
13368
|
+
await syncInstallation(ownerId, action.installationId, action.added);
|
|
11454
13369
|
} catch (err) {
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
13370
|
+
logger.error(
|
|
13371
|
+
`webhook handling failed for installation ${action.installationId}:`,
|
|
13372
|
+
err instanceof Error ? err.message : String(err)
|
|
13373
|
+
);
|
|
11458
13374
|
}
|
|
11459
13375
|
});
|
|
11460
13376
|
app.get("/repos", requireIssuerSecret, requireGithubApp, async (req, res) => {
|
|
@@ -11471,129 +13387,162 @@ async function startHttpServer(opts = {}) {
|
|
|
11471
13387
|
res.status(500).json({ error: "Could not list repos." });
|
|
11472
13388
|
}
|
|
11473
13389
|
});
|
|
11474
|
-
app.get(
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
if (!ownerId || !Number.isInteger(id)) {
|
|
11491
|
-
res.status(400).json({ error: "ownerId and a numeric id are required." });
|
|
11492
|
-
return;
|
|
13390
|
+
app.get(
|
|
13391
|
+
"/repos/alerts",
|
|
13392
|
+
requireIssuerSecret,
|
|
13393
|
+
requireGithubApp,
|
|
13394
|
+
async (req, res) => {
|
|
13395
|
+
const ownerId = ownerFrom(req);
|
|
13396
|
+
if (!ownerId) {
|
|
13397
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
13398
|
+
return;
|
|
13399
|
+
}
|
|
13400
|
+
try {
|
|
13401
|
+
res.status(200).json({ alerts: await listAlerts(db, ownerId) });
|
|
13402
|
+
} catch (err) {
|
|
13403
|
+
logger.error("alert list failed:", err instanceof Error ? err.message : String(err));
|
|
13404
|
+
res.status(500).json({ error: "Could not list alerts." });
|
|
13405
|
+
}
|
|
11493
13406
|
}
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
13407
|
+
);
|
|
13408
|
+
app.get(
|
|
13409
|
+
"/repos/:id",
|
|
13410
|
+
requireIssuerSecret,
|
|
13411
|
+
requireGithubApp,
|
|
13412
|
+
async (req, res) => {
|
|
13413
|
+
const ownerId = ownerFrom(req);
|
|
13414
|
+
const id = Number(req.params.id);
|
|
13415
|
+
if (!ownerId || !Number.isInteger(id)) {
|
|
13416
|
+
res.status(400).json({ error: "ownerId and a numeric id are required." });
|
|
11498
13417
|
return;
|
|
11499
13418
|
}
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
});
|
|
11506
|
-
res.status(200).json({
|
|
11507
|
-
repo: {
|
|
11508
|
-
...toDashboardRepo(row),
|
|
11509
|
-
deps: row.drift?.deps ?? [],
|
|
11510
|
-
transitiveRisks: row.drift?.transitive?.risks ?? [],
|
|
11511
|
-
conflicts: row.drift?.conflictsAtLatest ?? null,
|
|
11512
|
-
runs,
|
|
11513
|
-
workflow,
|
|
11514
|
-
workflowPath: WORKFLOW_PATH,
|
|
11515
|
-
setupUrl: newFileUrl(row.fullName, row.defaultBranch ?? "main", workflow)
|
|
13419
|
+
try {
|
|
13420
|
+
const row = await getRepo(db, ownerId, id);
|
|
13421
|
+
if (!row) {
|
|
13422
|
+
res.status(404).json({ error: "Repo not found." });
|
|
13423
|
+
return;
|
|
11516
13424
|
}
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
13425
|
+
const runs = await listRunsForRepo(db, ownerId, row.id);
|
|
13426
|
+
const workflow = renderWorkflow({
|
|
13427
|
+
installCommand: row.installCommand ?? void 0,
|
|
13428
|
+
armed: row.policy.enabled,
|
|
13429
|
+
autoMerge: row.policy.autoMerge
|
|
13430
|
+
});
|
|
13431
|
+
res.status(200).json({
|
|
13432
|
+
repo: {
|
|
13433
|
+
...toDashboardRepo(row),
|
|
13434
|
+
deps: row.drift?.deps ?? [],
|
|
13435
|
+
transitiveRisks: row.drift?.transitive?.risks ?? [],
|
|
13436
|
+
conflicts: row.drift?.conflictsAtLatest ?? null,
|
|
13437
|
+
runs,
|
|
13438
|
+
workflow,
|
|
13439
|
+
workflowPath: WORKFLOW_PATH,
|
|
13440
|
+
setupUrl: newFileUrl(row.fullName, row.defaultBranch ?? "main", workflow)
|
|
13441
|
+
}
|
|
13442
|
+
});
|
|
13443
|
+
} catch (err) {
|
|
13444
|
+
logger.error("repo read failed:", err instanceof Error ? err.message : String(err));
|
|
13445
|
+
res.status(500).json({ error: "Could not read repo." });
|
|
13446
|
+
}
|
|
11529
13447
|
}
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
13448
|
+
);
|
|
13449
|
+
app.get(
|
|
13450
|
+
"/repos/:id/brief",
|
|
13451
|
+
requireIssuerSecret,
|
|
13452
|
+
requireGithubApp,
|
|
13453
|
+
async (req, res) => {
|
|
13454
|
+
const ownerId = ownerFrom(req);
|
|
13455
|
+
const id = Number(req.params.id);
|
|
13456
|
+
if (!ownerId || !Number.isInteger(id)) {
|
|
13457
|
+
res.status(400).json({ error: "ownerId and a numeric id are required." });
|
|
11534
13458
|
return;
|
|
11535
13459
|
}
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
return;
|
|
13460
|
+
try {
|
|
13461
|
+
const row = await getRepo(db, ownerId, id);
|
|
13462
|
+
if (!row) {
|
|
13463
|
+
res.status(404).json({ error: "Repo not found." });
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
13466
|
+
res.status(200).json(await briefRepo(db, row.drift ?? null));
|
|
13467
|
+
} catch (err) {
|
|
13468
|
+
logger.error("repo brief failed:", err instanceof Error ? err.message : String(err));
|
|
13469
|
+
res.status(500).json({ error: "Could not build the migration brief." });
|
|
13470
|
+
}
|
|
11548
13471
|
}
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
13472
|
+
);
|
|
13473
|
+
app.post(
|
|
13474
|
+
"/repos/:id/scan",
|
|
13475
|
+
requireIssuerSecret,
|
|
13476
|
+
requireGithubApp,
|
|
13477
|
+
async (req, res) => {
|
|
13478
|
+
const ownerId = ownerFrom(req);
|
|
13479
|
+
const id = Number(req.params.id);
|
|
13480
|
+
if (!ownerId || !Number.isInteger(id)) {
|
|
13481
|
+
res.status(400).json({ error: "ownerId and a numeric id are required." });
|
|
11553
13482
|
return;
|
|
11554
13483
|
}
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
res.status(400).json({ error: "ownerId, a numeric id, and a complete policy are required." });
|
|
11568
|
-
return;
|
|
13484
|
+
try {
|
|
13485
|
+
const row = await getRepo(db, ownerId, id);
|
|
13486
|
+
if (!row) {
|
|
13487
|
+
res.status(404).json({ error: "Repo not found." });
|
|
13488
|
+
return;
|
|
13489
|
+
}
|
|
13490
|
+
const result = await scanRepo(db, row);
|
|
13491
|
+
res.status(200).json({ result });
|
|
13492
|
+
} catch (err) {
|
|
13493
|
+
logger.error("repo scan failed:", err instanceof Error ? err.message : String(err));
|
|
13494
|
+
res.status(500).json({ error: "Could not scan repo." });
|
|
13495
|
+
}
|
|
11569
13496
|
}
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
13497
|
+
);
|
|
13498
|
+
app.patch(
|
|
13499
|
+
"/repos/:id",
|
|
13500
|
+
requireIssuerSecret,
|
|
13501
|
+
requireGithubApp,
|
|
13502
|
+
async (req, res) => {
|
|
13503
|
+
const ownerId = ownerFrom(req);
|
|
13504
|
+
const id = Number(req.params.id);
|
|
13505
|
+
const policy = parsePolicy((req.body ?? {}).policy);
|
|
13506
|
+
if (!ownerId || !Number.isInteger(id) || !policy) {
|
|
13507
|
+
res.status(400).json({ error: "ownerId, a numeric id, and a complete policy are required." });
|
|
11574
13508
|
return;
|
|
11575
13509
|
}
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
13510
|
+
try {
|
|
13511
|
+
const updated = await setRepoPolicy(db, ownerId, id, policy);
|
|
13512
|
+
if (!updated) {
|
|
13513
|
+
res.status(404).json({ error: "Repo not found." });
|
|
13514
|
+
return;
|
|
13515
|
+
}
|
|
13516
|
+
res.status(200).json({ policy });
|
|
13517
|
+
} catch (err) {
|
|
13518
|
+
logger.error(
|
|
13519
|
+
"repo policy update failed:",
|
|
13520
|
+
err instanceof Error ? err.message : String(err)
|
|
13521
|
+
);
|
|
13522
|
+
res.status(500).json({ error: "Could not update policy." });
|
|
13523
|
+
}
|
|
11588
13524
|
}
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
13525
|
+
);
|
|
13526
|
+
app.delete(
|
|
13527
|
+
"/repos/:id",
|
|
13528
|
+
requireIssuerSecret,
|
|
13529
|
+
requireGithubApp,
|
|
13530
|
+
async (req, res) => {
|
|
13531
|
+
const ownerId = ownerFrom(req);
|
|
13532
|
+
const id = Number(req.params.id);
|
|
13533
|
+
if (!ownerId || !Number.isInteger(id)) {
|
|
13534
|
+
res.status(400).json({ error: "ownerId and a numeric id are required." });
|
|
13535
|
+
return;
|
|
13536
|
+
}
|
|
13537
|
+
try {
|
|
13538
|
+
const removed = await deleteRepo(db, ownerId, id);
|
|
13539
|
+
res.status(removed ? 200 : 404).json(removed ? { removed: true } : { error: "Repo not found." });
|
|
13540
|
+
} catch (err) {
|
|
13541
|
+
logger.error("repo delete failed:", err instanceof Error ? err.message : String(err));
|
|
13542
|
+
res.status(500).json({ error: "Could not disconnect repo." });
|
|
13543
|
+
}
|
|
11595
13544
|
}
|
|
11596
|
-
|
|
13545
|
+
);
|
|
11597
13546
|
app.get("/selection-policy", requireIssuerSecret, async (req, res) => {
|
|
11598
13547
|
const ownerId = ownerFrom(req);
|
|
11599
13548
|
if (!ownerId) {
|
|
@@ -11603,7 +13552,10 @@ async function startHttpServer(opts = {}) {
|
|
|
11603
13552
|
try {
|
|
11604
13553
|
res.status(200).json({ policy: await getSelectionPolicy(db, ownerId) });
|
|
11605
13554
|
} catch (err) {
|
|
11606
|
-
logger.error(
|
|
13555
|
+
logger.error(
|
|
13556
|
+
"selection policy read failed:",
|
|
13557
|
+
err instanceof Error ? err.message : String(err)
|
|
13558
|
+
);
|
|
11607
13559
|
res.status(500).json({ error: "Could not read policy." });
|
|
11608
13560
|
}
|
|
11609
13561
|
});
|
|
@@ -11618,75 +13570,117 @@ async function startHttpServer(opts = {}) {
|
|
|
11618
13570
|
await setSelectionPolicy(db, ownerId, policy);
|
|
11619
13571
|
res.status(200).json({ policy });
|
|
11620
13572
|
} catch (err) {
|
|
11621
|
-
logger.error(
|
|
13573
|
+
logger.error(
|
|
13574
|
+
"selection policy write failed:",
|
|
13575
|
+
err instanceof Error ? err.message : String(err)
|
|
13576
|
+
);
|
|
11622
13577
|
res.status(500).json({ error: "Could not save policy." });
|
|
11623
13578
|
}
|
|
11624
13579
|
});
|
|
11625
|
-
app.
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
13580
|
+
app.get(
|
|
13581
|
+
"/selection-policy/conformance",
|
|
13582
|
+
requireIssuerSecret,
|
|
13583
|
+
async (req, res) => {
|
|
13584
|
+
const ownerId = ownerFrom(req);
|
|
13585
|
+
if (!ownerId) {
|
|
13586
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
13587
|
+
return;
|
|
13588
|
+
}
|
|
13589
|
+
try {
|
|
13590
|
+
res.status(200).json(await repoConformance(db, ownerId));
|
|
13591
|
+
} catch (err) {
|
|
13592
|
+
logger.error("conformance read failed:", err instanceof Error ? err.message : String(err));
|
|
13593
|
+
res.status(500).json({ error: "Could not read conformance." });
|
|
13594
|
+
}
|
|
11630
13595
|
}
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
13596
|
+
);
|
|
13597
|
+
app.post(
|
|
13598
|
+
"/upgrade-plan",
|
|
13599
|
+
ipLimiter,
|
|
13600
|
+
auth,
|
|
13601
|
+
keyLimiter,
|
|
13602
|
+
quota,
|
|
13603
|
+
async (req, res) => {
|
|
13604
|
+
const deps = parseDepsInput((req.body ?? {}).deps);
|
|
13605
|
+
if (Object.keys(deps).length === 0) {
|
|
13606
|
+
res.status(400).json({ error: 'deps is required: { "package": "range", \u2026 }' });
|
|
13607
|
+
return;
|
|
13608
|
+
}
|
|
11634
13609
|
const ownerId = req.lurqKey?.ownerId ?? null;
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
res.status(403).json({ error: "This key has no account attached." });
|
|
11654
|
-
return;
|
|
11655
|
-
}
|
|
11656
|
-
const { runs, rejected } = parseUpgradeRuns((req.body ?? {}).runs, MAX_RUNS_PER_POST);
|
|
11657
|
-
if (runs.length === 0) {
|
|
11658
|
-
res.status(400).json({ error: "runs must be a non-empty array of upgrade results." });
|
|
11659
|
-
return;
|
|
13610
|
+
try {
|
|
13611
|
+
const drift = await computeDrift(db, [{ path: "package.json", deps }], null, ownerId);
|
|
13612
|
+
const brief = await briefRepo(db, drift);
|
|
13613
|
+
const repoFullName = parseRepoFullName((req.body ?? {}).repo);
|
|
13614
|
+
const policy = ownerId && repoFullName ? await findPolicyByFullName(db, ownerId, repoFullName) : null;
|
|
13615
|
+
const scoped = applyScope(brief.upgrades, policy);
|
|
13616
|
+
res.status(200).json({
|
|
13617
|
+
...brief,
|
|
13618
|
+
...scoped,
|
|
13619
|
+
// Surfaced so CI can log what lurq had no opinion on. An upgrade we do
|
|
13620
|
+
// not know about must not look like an upgrade we cleared.
|
|
13621
|
+
untracked: Object.keys(deps).length - drift.depsTracked
|
|
13622
|
+
});
|
|
13623
|
+
void recordUsage(db, ownerId, "upgrade-plan");
|
|
13624
|
+
} catch (err) {
|
|
13625
|
+
logger.error("upgrade plan failed:", err instanceof Error ? err.message : String(err));
|
|
13626
|
+
res.status(500).json({ error: "Could not build the upgrade plan." });
|
|
13627
|
+
}
|
|
11660
13628
|
}
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
13629
|
+
);
|
|
13630
|
+
app.post(
|
|
13631
|
+
"/upgrade-runs",
|
|
13632
|
+
ipLimiter,
|
|
13633
|
+
auth,
|
|
13634
|
+
keyLimiter,
|
|
13635
|
+
quota,
|
|
13636
|
+
async (req, res) => {
|
|
13637
|
+
const ownerId = req.lurqKey?.ownerId ?? null;
|
|
13638
|
+
if (!ownerId) {
|
|
13639
|
+
res.status(403).json({ error: "This key has no account attached." });
|
|
13640
|
+
return;
|
|
13641
|
+
}
|
|
13642
|
+
const { runs, rejected } = parseUpgradeRuns((req.body ?? {}).runs, MAX_RUNS_PER_POST);
|
|
13643
|
+
if (runs.length === 0) {
|
|
13644
|
+
res.status(400).json({ error: "runs must be a non-empty array of upgrade results." });
|
|
13645
|
+
return;
|
|
13646
|
+
}
|
|
13647
|
+
try {
|
|
13648
|
+
const repoIds = /* @__PURE__ */ new Map();
|
|
13649
|
+
for (const run of runs) {
|
|
13650
|
+
if (!repoIds.has(run.repoFullName)) {
|
|
13651
|
+
repoIds.set(
|
|
13652
|
+
run.repoFullName,
|
|
13653
|
+
await findRepoIdByFullName(db, ownerId, run.repoFullName)
|
|
13654
|
+
);
|
|
13655
|
+
}
|
|
11666
13656
|
}
|
|
13657
|
+
const recorded = await recordUpgradeRuns(
|
|
13658
|
+
db,
|
|
13659
|
+
runs.map((run) => ({ ...run, ownerId, repoId: repoIds.get(run.repoFullName) ?? null }))
|
|
13660
|
+
);
|
|
13661
|
+
res.status(200).json({ recorded, rejected });
|
|
13662
|
+
void recordUsage(db, ownerId, "upgrade-runs");
|
|
13663
|
+
} catch (err) {
|
|
13664
|
+
logger.error(
|
|
13665
|
+
"upgrade run record failed:",
|
|
13666
|
+
err instanceof Error ? err.message : String(err)
|
|
13667
|
+
);
|
|
13668
|
+
res.status(500).json({ error: "Could not record the upgrade runs." });
|
|
11667
13669
|
}
|
|
11668
|
-
const recorded = await recordUpgradeRuns(
|
|
11669
|
-
db,
|
|
11670
|
-
runs.map((run) => ({ ...run, ownerId, repoId: repoIds.get(run.repoFullName) ?? null }))
|
|
11671
|
-
);
|
|
11672
|
-
res.status(200).json({ recorded, rejected });
|
|
11673
|
-
} catch (err) {
|
|
11674
|
-
logger.error("upgrade run record failed:", err instanceof Error ? err.message : String(err));
|
|
11675
|
-
res.status(500).json({ error: "Could not record the upgrade runs." });
|
|
11676
13670
|
}
|
|
11677
|
-
|
|
11678
|
-
app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
|
|
11679
|
-
const
|
|
13671
|
+
);
|
|
13672
|
+
app.post("/mcp", ipLimiter, auth, keyLimiter, quota, async (req, res) => {
|
|
13673
|
+
const server2 = buildMcpServer(db, { ownerId: req.lurqKey?.ownerId ?? null });
|
|
11680
13674
|
const transport = new StreamableHTTPServerTransport({
|
|
11681
13675
|
sessionIdGenerator: void 0,
|
|
11682
13676
|
enableJsonResponse: true
|
|
11683
13677
|
});
|
|
11684
13678
|
res.on("close", () => {
|
|
11685
13679
|
void transport.close();
|
|
11686
|
-
void
|
|
13680
|
+
void server2.close();
|
|
11687
13681
|
});
|
|
11688
13682
|
try {
|
|
11689
|
-
await
|
|
13683
|
+
await server2.connect(transport);
|
|
11690
13684
|
await transport.handleRequest(req, res, req.body);
|
|
11691
13685
|
} catch (err) {
|
|
11692
13686
|
logger.error("mcp request failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -11696,9 +13690,41 @@ async function startHttpServer(opts = {}) {
|
|
|
11696
13690
|
app.all("/mcp", (_req, res) => {
|
|
11697
13691
|
res.status(405).json(rpcError(-32e3, "Method not allowed."));
|
|
11698
13692
|
});
|
|
11699
|
-
app.
|
|
13693
|
+
app.use((err, req, res, _next) => {
|
|
13694
|
+
const { status, body, clientFault } = errorEnvelope(err, req.path);
|
|
13695
|
+
if (!clientFault) {
|
|
13696
|
+
logger.error(`unhandled request error (${req.method} ${req.path}):`, err.message);
|
|
13697
|
+
}
|
|
13698
|
+
if (res.headersSent) return;
|
|
13699
|
+
res.status(status).json(body);
|
|
13700
|
+
});
|
|
13701
|
+
const server = app.listen(port, () => {
|
|
11700
13702
|
logger.info(`lurq HTTP MCP server listening on :${port}/mcp`);
|
|
11701
13703
|
});
|
|
13704
|
+
server.on("error", (err) => {
|
|
13705
|
+
logger.error(`could not listen on :${port}: ${err.message}`);
|
|
13706
|
+
process.exit(1);
|
|
13707
|
+
});
|
|
13708
|
+
const FORCE_EXIT_MS = 1e4;
|
|
13709
|
+
let shuttingDown = false;
|
|
13710
|
+
const shutdown = (signal) => {
|
|
13711
|
+
if (shuttingDown) return;
|
|
13712
|
+
shuttingDown = true;
|
|
13713
|
+
logger.info(`${signal} received, draining\u2026`);
|
|
13714
|
+
const force = setTimeout(() => {
|
|
13715
|
+
logger.warn("drain timed out, exiting anyway");
|
|
13716
|
+
process.exit(0);
|
|
13717
|
+
}, FORCE_EXIT_MS);
|
|
13718
|
+
force.unref();
|
|
13719
|
+
server.close(() => {
|
|
13720
|
+
void closeDb().then(
|
|
13721
|
+
() => process.exit(0),
|
|
13722
|
+
() => process.exit(0)
|
|
13723
|
+
);
|
|
13724
|
+
});
|
|
13725
|
+
};
|
|
13726
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
13727
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
11702
13728
|
}
|
|
11703
13729
|
var init_http2 = __esm({
|
|
11704
13730
|
"src/mcp/http.ts"() {
|
|
@@ -11706,6 +13732,7 @@ var init_http2 = __esm({
|
|
|
11706
13732
|
init_esm_shims();
|
|
11707
13733
|
init_config();
|
|
11708
13734
|
init_logger();
|
|
13735
|
+
init_capabilities();
|
|
11709
13736
|
init_apiKeys();
|
|
11710
13737
|
init_outcomes();
|
|
11711
13738
|
init_packages();
|
|
@@ -11713,15 +13740,21 @@ var init_http2 = __esm({
|
|
|
11713
13740
|
init_repos();
|
|
11714
13741
|
init_selectionPolicy();
|
|
11715
13742
|
init_parse();
|
|
13743
|
+
init_conformance();
|
|
11716
13744
|
init_usage();
|
|
13745
|
+
init_subscriptions();
|
|
13746
|
+
init_stripe();
|
|
13747
|
+
init_plans();
|
|
11717
13748
|
init_client();
|
|
11718
13749
|
init_app();
|
|
11719
13750
|
init_brief();
|
|
11720
13751
|
init_drift();
|
|
13752
|
+
init_askSpend();
|
|
11721
13753
|
init_scope();
|
|
11722
13754
|
init_runs();
|
|
11723
13755
|
init_upgradeRuns();
|
|
11724
13756
|
init_manifests();
|
|
13757
|
+
init_webhook();
|
|
11725
13758
|
init_workflow();
|
|
11726
13759
|
init_repoScan();
|
|
11727
13760
|
init_server();
|
|
@@ -11731,7 +13764,7 @@ var init_http2 = __esm({
|
|
|
11731
13764
|
|
|
11732
13765
|
// src/bin/lurq.ts
|
|
11733
13766
|
init_esm_shims();
|
|
11734
|
-
import { readFileSync as
|
|
13767
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
11735
13768
|
import updateNotifier from "update-notifier";
|
|
11736
13769
|
|
|
11737
13770
|
// src/cli/index.ts
|
|
@@ -11770,6 +13803,10 @@ function buildProgram() {
|
|
|
11770
13803
|
clearUserConfig2() ? `Removed ${path2}. Agent MCP configs still hold the key; re-run \`lurq setup\` to change them.` : "No stored API key on this machine."
|
|
11771
13804
|
);
|
|
11772
13805
|
});
|
|
13806
|
+
program.command("can").argument("[query...]", "what you are trying to do, in plain words").description("find the lurq capability for what you are trying to do").option("--json", "output matches as JSON").action(async (query, opts) => {
|
|
13807
|
+
const { runCan: runCan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
13808
|
+
runCan2(query.join(" "), opts);
|
|
13809
|
+
});
|
|
11773
13810
|
program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | promising | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
|
|
11774
13811
|
const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
11775
13812
|
await runRecommend2(need, opts);
|
|
@@ -11813,12 +13850,31 @@ function buildProgram() {
|
|
|
11813
13850
|
const plan = await buildUpgradePlan2(dir, { url: opts.url, apiKey: opts.apiKey, repo: opts.repo });
|
|
11814
13851
|
console.log(opts.json ? JSON.stringify(plan, null, 2) : formatUpgradePlan2(plan));
|
|
11815
13852
|
});
|
|
11816
|
-
program.command("check-upgrade").argument("[dir]", "project directory to scan (default: current)", ".").description("do these upgrades remove symbols your code actually references?").option("--plan <file>", "targets from `upgrade-plan --json`").option("--upgrade <spec...>", "pkg@from..to (repeatable), e.g. commander@11.1.0..12.1.0").option("--json", "output the report as JSON").option("--exit-code", "exit 1 when the report is not safe (for CI)").action(
|
|
13853
|
+
program.command("check-upgrade").argument("[dir]", "project directory to scan (default: current)", ".").description("do these upgrades remove symbols your code actually references?").option("--plan <file>", "targets from `upgrade-plan --json`").option("--upgrade <spec...>", "pkg@from..to (repeatable), e.g. commander@11.1.0..12.1.0").option("--json", "output the report as JSON").option("--exit-code", "exit 1 when the report is not safe (for CI)").option("--report", "send the result to your lurq dashboard (CI; needs an API key)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--api-key <key>", "hosted API key (defaults to $LURQ_API_KEY)").action(
|
|
11817
13854
|
async (dir, opts) => {
|
|
11818
13855
|
const { runCheckUpgrade: runCheckUpgrade2 } = await Promise.resolve().then(() => (init_checkUpgrade(), checkUpgrade_exports));
|
|
11819
13856
|
await runCheckUpgrade2(dir, opts);
|
|
11820
13857
|
}
|
|
11821
13858
|
);
|
|
13859
|
+
program.command("check-release").argument("[dir]", "package directory (default: current)", ".").description("is the version you are about to publish honest about what changed?").option("--against <version>", "compare with this published version (default: latest)").option("--json", "output the check as JSON").option("--exit-code", "exit 1 when the bump is understated (for CI / prepublish)").action(
|
|
13860
|
+
async (dir, opts) => {
|
|
13861
|
+
const { checkRelease: checkRelease2, formatReleaseCheck: formatReleaseCheck2 } = await Promise.resolve().then(() => (init_release(), release_exports));
|
|
13862
|
+
const check2 = await checkRelease2(dir, { against: opts.against });
|
|
13863
|
+
console.log(opts.json ? JSON.stringify(check2, null, 2) : formatReleaseCheck2(check2));
|
|
13864
|
+
if (opts.exitCode && check2.verdict !== "ok") process.exitCode = 1;
|
|
13865
|
+
}
|
|
13866
|
+
);
|
|
13867
|
+
program.command("check-api").argument("[spec]", "OpenAPI document (default: the first one found)").description("does this change break the callers of your own API?").option("--against <rev>", "git revision to compare with (default: HEAD)").option("-C, --dir <dir>", "repository directory (default: current)", ".").option("--json", "output the check as JSON").option("--exit-code", "exit 1 on a breaking change (for CI)").action(
|
|
13868
|
+
async (spec, opts) => {
|
|
13869
|
+
const { runApiCheck: runApiCheck2 } = await Promise.resolve().then(() => (init_check2(), check_exports));
|
|
13870
|
+
const { formatApiCheck: formatApiCheck2 } = await Promise.resolve().then(() => (init_diff3(), diff_exports));
|
|
13871
|
+
const check2 = await runApiCheck2(opts.dir ?? ".", { spec, against: opts.against });
|
|
13872
|
+
console.log(
|
|
13873
|
+
opts.json ? JSON.stringify(check2, null, 2) : formatApiCheck2(check2, `api check (vs ${check2.against})`)
|
|
13874
|
+
);
|
|
13875
|
+
if (opts.exitCode && check2.verdict !== "ok") process.exitCode = 1;
|
|
13876
|
+
}
|
|
13877
|
+
);
|
|
11822
13878
|
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) => {
|
|
11823
13879
|
const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
11824
13880
|
runWeights2(opts);
|
|
@@ -11848,7 +13904,7 @@ init_config();
|
|
|
11848
13904
|
// src/core/gate.ts
|
|
11849
13905
|
init_esm_shims();
|
|
11850
13906
|
init_constants();
|
|
11851
|
-
import { createHash as createHash10, timingSafeEqual as
|
|
13907
|
+
import { createHash as createHash10, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
11852
13908
|
var PRE_LAUNCH = false;
|
|
11853
13909
|
var OWNER_HASH = "9675c37e56783e747e4a89ee3919d95d1364f2e5dd0f14501b0f878f8cc3682b";
|
|
11854
13910
|
var ALLOWED = /* @__PURE__ */ new Set([
|
|
@@ -11870,7 +13926,7 @@ function isOwner() {
|
|
|
11870
13926
|
if (!key) return false;
|
|
11871
13927
|
const got = createHash10("sha256").update(key).digest();
|
|
11872
13928
|
const want = Buffer.from(OWNER_HASH, "hex");
|
|
11873
|
-
return got.length === want.length &&
|
|
13929
|
+
return got.length === want.length && timingSafeEqual3(got, want);
|
|
11874
13930
|
}
|
|
11875
13931
|
function enforceGate(argv) {
|
|
11876
13932
|
if (!PRE_LAUNCH || isOwner()) return;
|
|
@@ -11918,7 +13974,7 @@ function notifyOnUpdate() {
|
|
|
11918
13974
|
if (quiet) return;
|
|
11919
13975
|
try {
|
|
11920
13976
|
const pkg = JSON.parse(
|
|
11921
|
-
|
|
13977
|
+
readFileSync10(new URL("../../package.json", import.meta.url), "utf8")
|
|
11922
13978
|
);
|
|
11923
13979
|
updateNotifier({ pkg }).notify();
|
|
11924
13980
|
} catch {
|