lopebase 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/lopebase.js +348 -31
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
<a href="https://www.npmjs.com/package/@lopebase/adapter">@lopebase/adapter</a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://allmcps.com/mcp/lopebase"><img src="https://allmcps.com/api/badge/lopebase?style=shield" alt="AllMCPs Verified"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
19
23
|
---
|
|
20
24
|
|
|
21
25
|
LopeBase is one place to see and run all of your products: traffic, search,
|
|
@@ -79,6 +83,7 @@ Made for web products, and fine with anything else:
|
|
|
79
83
|
| --- | --- |
|
|
80
84
|
| `npx lopebase` | Scan, connect, choose, and hand over to your agent. |
|
|
81
85
|
| `npx lopebase login` | Connect this computer to your workspace with a short code, creating an account if you need one. `--paste` to paste an agent token instead. |
|
|
86
|
+
| `npx lopebase update` | Update this project's LopeBase Adapter: what is new, then your agent does it. |
|
|
82
87
|
| `npx lopebase scan` | Show what the scan found. `--json` for scripts. |
|
|
83
88
|
| `npx lopebase prompt` | Print the setup prompt instead of starting an agent. |
|
|
84
89
|
| `npx lopebase mcp` | Run the LopeBase tools as a local MCP server (stdio) for any agent. |
|
package/dist/lopebase.js
CHANGED
|
@@ -502,7 +502,7 @@ var require_cross_spawn = __commonJS(function(exports, module) {
|
|
|
502
502
|
|
|
503
503
|
// bin/lopebase.ts
|
|
504
504
|
import { spawn as spawn2 } from "node:child_process";
|
|
505
|
-
import { readFileSync as
|
|
505
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
506
506
|
|
|
507
507
|
// ../../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
|
|
508
508
|
import { stripVTControlCharacters as S2 } from "node:util";
|
|
@@ -1205,6 +1205,86 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
1205
1205
|
} };
|
|
1206
1206
|
};
|
|
1207
1207
|
|
|
1208
|
+
// ../admin-adapter/src/releases.ts
|
|
1209
|
+
var ADAPTER_VERSION = "0.3.1", ADAPTER_RELEASES = [
|
|
1210
|
+
{
|
|
1211
|
+
version: "0.3.1",
|
|
1212
|
+
date: "2026-10-08",
|
|
1213
|
+
features: [
|
|
1214
|
+
"Reports its version to LopeBase, so the console can say when an update is out."
|
|
1215
|
+
],
|
|
1216
|
+
steps: []
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
version: "0.3.0",
|
|
1220
|
+
date: "2026-10-08",
|
|
1221
|
+
features: [
|
|
1222
|
+
"Service health: list the database, queues, workers, or other hosts the app needs, and LopeBase's uptime checks watch each one.",
|
|
1223
|
+
"httpCheck(id, url) for a service that has its own health address."
|
|
1224
|
+
],
|
|
1225
|
+
steps: [
|
|
1226
|
+
"Read the code for what the app cannot work without (database, queue, workers, third-party APIs called on every request) and show the human the list.",
|
|
1227
|
+
"Add them as `services` in createAdminAdapter: a check function each (a cheap query like select 1), or httpCheck for another host.",
|
|
1228
|
+
"After deploying, set the product's uptime check in LopeBase to <app>/api/lopebase/health."
|
|
1229
|
+
]
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
version: "0.2.0",
|
|
1233
|
+
date: "2026-10-01",
|
|
1234
|
+
features: [
|
|
1235
|
+
"The app's owner chooses how emails, names, phone numbers, and typed text reach LopeBase: in full, masked, or not at all."
|
|
1236
|
+
],
|
|
1237
|
+
breaking: [
|
|
1238
|
+
'An adapter with personal fields (email, name, phone, free text) will not start until `personalData` is set to "full", "masked", or "ids_only".'
|
|
1239
|
+
],
|
|
1240
|
+
steps: [
|
|
1241
|
+
"Ask the human how personal data may reach LopeBase (full, masked, or ids_only) and set `personalData` in createAdminAdapter to their answer.",
|
|
1242
|
+
'Mark free-text fields people wrote, like messages and notes, with personal: "text".'
|
|
1243
|
+
]
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
version: "0.1.2",
|
|
1247
|
+
date: "2026-09-30",
|
|
1248
|
+
features: [
|
|
1249
|
+
"Signed requests, the standard overview stats, and the conformance suite."
|
|
1250
|
+
],
|
|
1251
|
+
steps: []
|
|
1252
|
+
}
|
|
1253
|
+
];
|
|
1254
|
+
function compareVersions(a, b) {
|
|
1255
|
+
let parts = (v) => (v.match(/^\D*(\d+)\.(\d+)\.(\d+)/) ?? []).slice(1).map(Number), [x, y] = [parts(a), parts(b)];
|
|
1256
|
+
if (x.length !== 3)
|
|
1257
|
+
return y.length === 3 ? -1 : 0;
|
|
1258
|
+
if (y.length !== 3)
|
|
1259
|
+
return 1;
|
|
1260
|
+
for (let i = 0;i < 3; i++)
|
|
1261
|
+
if (x[i] !== y[i])
|
|
1262
|
+
return x[i] < y[i] ? -1 : 1;
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
function releasesSince(version) {
|
|
1266
|
+
return ADAPTER_RELEASES.filter((r) => !version || compareVersions(r.version, version) > 0).reverse();
|
|
1267
|
+
}
|
|
1268
|
+
function upgradePrompt(fromVersion) {
|
|
1269
|
+
let todo = releasesSince(fromVersion), breaking = todo.flatMap((r) => r.breaking ?? []), steps = todo.flatMap((r) => r.steps);
|
|
1270
|
+
return [
|
|
1271
|
+
`Update this app's LopeBase adapter (@lopebase/adapter) from ${fromVersion ?? "its current version"} to 0.3.1.`,
|
|
1272
|
+
"",
|
|
1273
|
+
"1. Install it with this repo's package manager, for example `npm install @lopebase/adapter@latest`. A 0.x range like ^0.1.2 does not pick up 0.2 or later on its own.",
|
|
1274
|
+
...breaking.length ? [
|
|
1275
|
+
"2. These changes would stop the adapter from starting; handle each one:",
|
|
1276
|
+
...breaking.map((b) => ` - ${b}`)
|
|
1277
|
+
] : ["2. Nothing in this update stops an existing adapter from starting."],
|
|
1278
|
+
...steps.length ? [
|
|
1279
|
+
"3. Adopt what is new, asking the human before each change:",
|
|
1280
|
+
...steps.map((s) => ` - ${s}`)
|
|
1281
|
+
] : ["3. Nothing to add in code."],
|
|
1282
|
+
"4. Run the app and `LOPEBASE_SIGNING_SECRET=... npx lopebase adapter check http://localhost:<port>/api/lopebase`, plus this repo's own tests. Fix anything that fails.",
|
|
1283
|
+
"5. Summarize what changed. Never print the signing secret."
|
|
1284
|
+
].join(`
|
|
1285
|
+
`);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1208
1288
|
// ../admin-adapter/src/security.ts
|
|
1209
1289
|
var SIGNATURE_HEADER = "lopebase-signature";
|
|
1210
1290
|
var enc = /* @__PURE__ */ new TextEncoder;
|
|
@@ -1704,6 +1784,46 @@ var SOURCE_RULES = {
|
|
|
1704
1784
|
},
|
|
1705
1785
|
why: "Error counts next to your traffic, so a bad deploy shows up."
|
|
1706
1786
|
},
|
|
1787
|
+
rollbar: {
|
|
1788
|
+
label: "Rollbar",
|
|
1789
|
+
rule: {
|
|
1790
|
+
deps: [
|
|
1791
|
+
/^rollbar$/,
|
|
1792
|
+
/^@rollbar\//,
|
|
1793
|
+
/^py:rollbar$/,
|
|
1794
|
+
/^gem:rollbar$/,
|
|
1795
|
+
/^composer:rollbar\//
|
|
1796
|
+
],
|
|
1797
|
+
env: [/ROLLBAR_ACCESS_TOKEN/]
|
|
1798
|
+
},
|
|
1799
|
+
why: "Error counts next to your traffic, so a bad deploy shows up."
|
|
1800
|
+
},
|
|
1801
|
+
honeybadger: {
|
|
1802
|
+
label: "Honeybadger",
|
|
1803
|
+
rule: {
|
|
1804
|
+
deps: [
|
|
1805
|
+
/^@honeybadger-io\//,
|
|
1806
|
+
/^py:honeybadger$/,
|
|
1807
|
+
/^gem:honeybadger$/,
|
|
1808
|
+
/^composer:honeybadger-io\//
|
|
1809
|
+
],
|
|
1810
|
+
env: [/HONEYBADGER_API_KEY/]
|
|
1811
|
+
},
|
|
1812
|
+
why: "Error counts next to your traffic, so a bad deploy shows up."
|
|
1813
|
+
},
|
|
1814
|
+
bugsnag: {
|
|
1815
|
+
label: "Bugsnag",
|
|
1816
|
+
rule: {
|
|
1817
|
+
deps: [
|
|
1818
|
+
/^@bugsnag\//,
|
|
1819
|
+
/^py:bugsnag$/,
|
|
1820
|
+
/^gem:bugsnag$/,
|
|
1821
|
+
/^composer:bugsnag\//
|
|
1822
|
+
],
|
|
1823
|
+
env: [/BUGSNAG_API_KEY/]
|
|
1824
|
+
},
|
|
1825
|
+
why: "Error counts next to your traffic, so a bad deploy shows up."
|
|
1826
|
+
},
|
|
1707
1827
|
stripe: {
|
|
1708
1828
|
label: "Stripe",
|
|
1709
1829
|
rule: {
|
|
@@ -2156,7 +2276,7 @@ function detectStack(facts) {
|
|
|
2156
2276
|
why: SOURCE_RULES.posthog.why
|
|
2157
2277
|
});
|
|
2158
2278
|
}
|
|
2159
|
-
if (!
|
|
2279
|
+
if (!["sentry", "rollbar", "honeybadger", "bugsnag"].some((id) => has(id))) {
|
|
2160
2280
|
if (gaps.push("No error logging found."), !listed("sentry"))
|
|
2161
2281
|
sources.push({
|
|
2162
2282
|
source: "sentry",
|
|
@@ -2168,6 +2288,38 @@ function detectStack(facts) {
|
|
|
2168
2288
|
}
|
|
2169
2289
|
return { stack: tidy, sources, gaps };
|
|
2170
2290
|
}
|
|
2291
|
+
var QUEUES = [
|
|
2292
|
+
[/^bullmq$|^bull$/, "Job queue (BullMQ)"],
|
|
2293
|
+
[/^inngest$/, "Background jobs (Inngest)"],
|
|
2294
|
+
[/^@trigger\.dev\//, "Background jobs (Trigger.dev)"],
|
|
2295
|
+
[/^@upstash\/qstash$/, "Message queue (QStash)"],
|
|
2296
|
+
[/^py:celery$/, "Job queue (Celery)"],
|
|
2297
|
+
[/^gem:sidekiq$/, "Job queue (Sidekiq)"]
|
|
2298
|
+
], NOT_A_SERVICE = /* @__PURE__ */ new Set(["Prisma", "Drizzle"]);
|
|
2299
|
+
function watchList(d, facts) {
|
|
2300
|
+
let slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), out = [];
|
|
2301
|
+
if (d.sources.some((s) => s.source === "uptime") || facts.siteUrl)
|
|
2302
|
+
out.push({ id: "site", label: facts.siteUrl ?? "Website", how: "address" });
|
|
2303
|
+
for (let s of d.stack)
|
|
2304
|
+
if (s.kind === "database" && !NOT_A_SERVICE.has(s.name))
|
|
2305
|
+
out.push({ id: slug(s.name), label: s.name, how: "service" });
|
|
2306
|
+
for (let [re, label] of QUEUES)
|
|
2307
|
+
if (facts.dependencies.some((dep) => re.test(dep)))
|
|
2308
|
+
out.push({
|
|
2309
|
+
id: slug(label.replace(/\s*\(.*\)/, "")) || "queue",
|
|
2310
|
+
label,
|
|
2311
|
+
how: "service"
|
|
2312
|
+
});
|
|
2313
|
+
let email = d.stack.find((s) => s.kind === "email" && s.name !== "Nodemailer");
|
|
2314
|
+
if (email)
|
|
2315
|
+
out.push({
|
|
2316
|
+
id: "email",
|
|
2317
|
+
label: `Email sending (${email.name})`,
|
|
2318
|
+
how: "service"
|
|
2319
|
+
});
|
|
2320
|
+
let seen = /* @__PURE__ */ new Set;
|
|
2321
|
+
return out.filter((w) => !seen.has(w.id) && seen.add(w.id));
|
|
2322
|
+
}
|
|
2171
2323
|
|
|
2172
2324
|
// src/device-login.ts
|
|
2173
2325
|
async function startDeviceLogin(url, opts = {}) {
|
|
@@ -2338,6 +2490,8 @@ function onboardingPrompt(input) {
|
|
|
2338
2490
|
`- Stack: ${stack}`,
|
|
2339
2491
|
`- Already in use: ${found.map((s) => s.label).join(", ") || "none of the sources LopeBase reads"}`,
|
|
2340
2492
|
`- The human wants connected: ${chosen.join(", ") || "whatever get_onboarding_status recommends"}`,
|
|
2493
|
+
`- Worth watching for uptime: ${(input.watch ?? []).map((w) => `${w.label} (${w.how === "address" ? "uptime check on its address" : "adapter health service"})`).join(", ") || "the site, if it has one"}`,
|
|
2494
|
+
`- LopeBase pageviews: ${input.pageviews ? "yes, the human wants them" : "not chosen; offer once, do not push"}`,
|
|
2341
2495
|
"",
|
|
2342
2496
|
"How to work:",
|
|
2343
2497
|
"1. Call get_onboarding_status first, and again after every step. Do the step it marks next.",
|
|
@@ -2346,7 +2500,9 @@ function onboardingPrompt(input) {
|
|
|
2346
2500
|
"4. When a step needs the human (a browser sign-in, a payment, a permission, a key you cannot create), call request_human_action, tell them in one or two plain sentences what to do and why, and wait for them.",
|
|
2347
2501
|
"5. Never print a secret in chat or write one to a tracked file. Pass keys straight to connect_source.",
|
|
2348
2502
|
"6. For a source the project does not use yet (for example analytics), explain what it adds, ask before installing it, then add it the way this codebase does things.",
|
|
2349
|
-
"7.
|
|
2503
|
+
"7. Uptime: confirm the list above against the code, and add anything it misses (a separate API or app host, a third-party API called on every request). Show the human the list. With the adapter, declare the services and use <app>/api/lopebase/health as the uptime address; otherwise use the site's address.",
|
|
2504
|
+
"8. Pageviews: if the human wants them, call get_pageview_snippet, add the tag once to the shared layout, deploy, and confirm with get_pageviews.",
|
|
2505
|
+
"9. Finish when get_onboarding_status says done. Summarize what is connected and anything left for later."
|
|
2350
2506
|
].join(`
|
|
2351
2507
|
`);
|
|
2352
2508
|
}
|
|
@@ -2695,7 +2851,28 @@ function scanProject(root, onProgress) {
|
|
|
2695
2851
|
}
|
|
2696
2852
|
|
|
2697
2853
|
// src/ui.ts
|
|
2698
|
-
var import_picocolors3 = __toESM(require_picocolors(), 1),
|
|
2854
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1), RAMP = [
|
|
2855
|
+
[214, 202, 245],
|
|
2856
|
+
[195, 177, 240],
|
|
2857
|
+
[176, 152, 235],
|
|
2858
|
+
[163, 135, 231],
|
|
2859
|
+
[146, 112, 220],
|
|
2860
|
+
[131, 92, 208],
|
|
2861
|
+
[123, 82, 201],
|
|
2862
|
+
[103, 58, 183]
|
|
2863
|
+
], BRAND = [163, 135, 231];
|
|
2864
|
+
function trueColor(env = process.env) {
|
|
2865
|
+
return /truecolor|24bit/i.test(env.COLORTERM ?? "");
|
|
2866
|
+
}
|
|
2867
|
+
function paint(rgb, text, bg = !1) {
|
|
2868
|
+
if (!import_picocolors3.default.isColorSupported)
|
|
2869
|
+
return text;
|
|
2870
|
+
if (!trueColor())
|
|
2871
|
+
return bg ? import_picocolors3.default.bgMagenta(import_picocolors3.default.black(text)) : import_picocolors3.default.magenta(text);
|
|
2872
|
+
let [r, g, b] = rgb;
|
|
2873
|
+
return `\x1B[${bg ? 48 : 38};2;${r};${g};${b}m${text}\x1B[${bg ? 49 : 39}m`;
|
|
2874
|
+
}
|
|
2875
|
+
var brand = (text) => paint(BRAND, text), badge = (text) => paint(BRAND, import_picocolors3.default.black(import_picocolors3.default.bold(` ${text} `)), !0), ramp = (i, text) => paint(RAMP[Math.min(i, RAMP.length - 1)], text), MARK = [
|
|
2699
2876
|
" ▗▖ ▗▖ ",
|
|
2700
2877
|
" ▐█▖▗█▌ ",
|
|
2701
2878
|
" ▜██▛ ",
|
|
@@ -2711,11 +2888,11 @@ function motionAllowed(argv = process.argv, env = process.env, isTTY = Boolean(p
|
|
|
2711
2888
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2712
2889
|
function logoLines(opts) {
|
|
2713
2890
|
let right = {
|
|
2714
|
-
3: `${import_picocolors3.default.bold("Lope")}${
|
|
2891
|
+
3: `${import_picocolors3.default.bold("Lope")}${brand("Base")}${opts.version ? import_picocolors3.default.dim(` v${opts.version}`) : ""}`,
|
|
2715
2892
|
4: import_picocolors3.default.dim(opts.tagline)
|
|
2716
2893
|
};
|
|
2717
2894
|
return MARK.map((row, i) => {
|
|
2718
|
-
let mark = HORIZON.has(i) ? import_picocolors3.default.
|
|
2895
|
+
let mark = HORIZON.has(i) ? import_picocolors3.default.dim(ramp(i, row)) : ramp(i, row);
|
|
2719
2896
|
return right[i] ? `${mark} ${right[i]}` : mark;
|
|
2720
2897
|
});
|
|
2721
2898
|
}
|
|
@@ -2729,7 +2906,7 @@ ${lines.join(`
|
|
|
2729
2906
|
`);
|
|
2730
2907
|
return;
|
|
2731
2908
|
}
|
|
2732
|
-
let blank = MARK.map((r) => " ".repeat(r.length)), frame = (shown) => MARK.map((row, i) => shown.has(i) ? HORIZON.has(i) ? import_picocolors3.default.
|
|
2909
|
+
let blank = MARK.map((r) => " ".repeat(r.length)), frame = (shown) => MARK.map((row, i) => shown.has(i) ? HORIZON.has(i) ? import_picocolors3.default.dim(ramp(i, row)) : ramp(i, row) : blank[i]), up = `\x1B[${MARK.length}A`, onInterrupt = () => {
|
|
2733
2910
|
write(`\x1B[?25h
|
|
2734
2911
|
`), process.exit(130);
|
|
2735
2912
|
};
|
|
@@ -2805,20 +2982,85 @@ var PLAN = [
|
|
|
2805
2982
|
}
|
|
2806
2983
|
];
|
|
2807
2984
|
function planLines() {
|
|
2808
|
-
return PLAN.map((s, i) => `${
|
|
2985
|
+
return PLAN.map((s, i) => `${brand(String(i + 1))} ${import_picocolors3.default.bold(s.title)}
|
|
2809
2986
|
${import_picocolors3.default.dim(s.detail)}`);
|
|
2810
2987
|
}
|
|
2988
|
+
function stepRail(n, total = PLAN.length) {
|
|
2989
|
+
return Array.from({ length: total }, (_, i) => i < n ? brand("◆") : import_picocolors3.default.dim("◇")).join(import_picocolors3.default.dim(" ── "));
|
|
2990
|
+
}
|
|
2811
2991
|
function stepTitle(n) {
|
|
2812
|
-
return `${import_picocolors3.default.dim(`Step ${n} of ${PLAN.length}`)} ${import_picocolors3.default.bold(PLAN[n - 1].title)}`;
|
|
2992
|
+
return `${stepRail(n)} ${import_picocolors3.default.dim(`Step ${n} of ${PLAN.length}`)} ${import_picocolors3.default.bold(PLAN[n - 1].title)}`;
|
|
2993
|
+
}
|
|
2994
|
+
async function horizon(opts) {
|
|
2995
|
+
let write = opts.write ?? ((s) => process.stdout.write(s)), width = opts.width ?? 28, line = (n) => Array.from({ length: n }, (_, i) => ramp(Math.floor(i / width * RAMP.length), "━")).join("");
|
|
2996
|
+
if (!opts.motion) {
|
|
2997
|
+
write(` ${line(width)}
|
|
2998
|
+
`);
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
for (let n = 2;n <= width; n += 2)
|
|
3002
|
+
write(`\r ${line(n)}`), await sleep(10);
|
|
3003
|
+
write(`
|
|
3004
|
+
`);
|
|
3005
|
+
}
|
|
3006
|
+
function watchRows(items) {
|
|
3007
|
+
return items.map((w) => w.how === "address" ? `${brand("◉")} ${import_picocolors3.default.bold(w.label)} ${import_picocolors3.default.dim("uptime check, every 15 minutes")}` : `${brand("○")} ${import_picocolors3.default.bold(w.label)} ${import_picocolors3.default.dim("a service in the adapter's health list")}`);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/update.ts
|
|
3011
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3012
|
+
import path3 from "node:path";
|
|
3013
|
+
var readJson2 = (file) => {
|
|
3014
|
+
try {
|
|
3015
|
+
return JSON.parse(readFileSync3(file, "utf8"));
|
|
3016
|
+
} catch {
|
|
3017
|
+
return null;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
function findAdapterInstalls(root) {
|
|
3021
|
+
let out = [];
|
|
3022
|
+
for (let dir of packageDirs(root)) {
|
|
3023
|
+
let pkg = readJson2(path3.join(dir, "package.json"));
|
|
3024
|
+
if (!pkg)
|
|
3025
|
+
continue;
|
|
3026
|
+
let range = {
|
|
3027
|
+
...pkg.dependencies,
|
|
3028
|
+
...pkg.devDependencies
|
|
3029
|
+
}["@lopebase/adapter"];
|
|
3030
|
+
if (!range || range.startsWith("workspace:"))
|
|
3031
|
+
continue;
|
|
3032
|
+
let installed = [dir, root].map((d) => readJson2(path3.join(d, "node_modules/@lopebase/adapter/package.json"))).find((p) => typeof p?.version === "string")?.version;
|
|
3033
|
+
out.push({ dir: path3.relative(root, dir) || ".", range, installed });
|
|
3034
|
+
}
|
|
3035
|
+
return out;
|
|
3036
|
+
}
|
|
3037
|
+
function runningVersion(i) {
|
|
3038
|
+
return i.installed ?? i.range.replace(/^[^\d]*/, "");
|
|
3039
|
+
}
|
|
3040
|
+
var isBehind = (running, latest) => compareVersions(running, latest) < 0;
|
|
3041
|
+
async function latestOnNpm(name, timeoutMs = 1500, fetchImpl = fetch) {
|
|
3042
|
+
try {
|
|
3043
|
+
let res = await fetchImpl(`https://registry.npmjs.org/${name}/latest`, {
|
|
3044
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3045
|
+
headers: { accept: "application/json" }
|
|
3046
|
+
});
|
|
3047
|
+
if (!res.ok)
|
|
3048
|
+
return;
|
|
3049
|
+
let body = await res.json();
|
|
3050
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
3051
|
+
} catch {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
2813
3054
|
}
|
|
2814
3055
|
|
|
2815
3056
|
// bin/lopebase.ts
|
|
2816
3057
|
var [command = "init", ...rest] = process.argv.slice(2), HELP = `
|
|
2817
|
-
${
|
|
3058
|
+
${badge("LopeBase")} Set up LopeBase from this project's folder.
|
|
2818
3059
|
|
|
2819
3060
|
${import_picocolors4.default.bold("Usage")}
|
|
2820
3061
|
npx lopebase scan, connect, and hand over to your agent
|
|
2821
3062
|
npx lopebase login connect this computer (--paste for a token)
|
|
3063
|
+
npx lopebase update update this project's LopeBase Adapter to the latest
|
|
2822
3064
|
npx lopebase scan [--json] show what the scan found
|
|
2823
3065
|
npx lopebase prompt print the setup prompt instead
|
|
2824
3066
|
npx lopebase mcp run the LopeBase tools as a stdio MCP server
|
|
@@ -2831,15 +3073,15 @@ ${import_picocolors4.default.bold("Options")}
|
|
|
2831
3073
|
--no-motion no animation (also LOPEBASE_NO_MOTION=1)
|
|
2832
3074
|
|
|
2833
3075
|
${import_picocolors4.default.bold("Learn more")}
|
|
2834
|
-
${
|
|
3076
|
+
${brand("https://lopebase.com")}
|
|
2835
3077
|
Help: contact@jackalope.digital
|
|
2836
3078
|
`, flag = (name) => rest.includes(`--${name}`), option = (name) => {
|
|
2837
3079
|
let i = rest.indexOf(`--${name}`);
|
|
2838
3080
|
return i >= 0 ? rest[i + 1] : rest.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
|
|
2839
|
-
}, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.";
|
|
3081
|
+
}, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.", PAGEVIEWS = "LopeBase pageviews";
|
|
2840
3082
|
function version() {
|
|
2841
3083
|
try {
|
|
2842
|
-
return JSON.parse(
|
|
3084
|
+
return JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
2843
3085
|
} catch {
|
|
2844
3086
|
return;
|
|
2845
3087
|
}
|
|
@@ -2898,7 +3140,7 @@ async function login() {
|
|
|
2898
3140
|
let device = await startDeviceLogin(url);
|
|
2899
3141
|
if (device) {
|
|
2900
3142
|
Me([
|
|
2901
|
-
`Your code: ${import_picocolors4.default.bold(
|
|
3143
|
+
`Your code: ${import_picocolors4.default.bold(brand(device.userCode))}`,
|
|
2902
3144
|
"",
|
|
2903
3145
|
"We opened LopeBase in your browser. Sign in, or create a free",
|
|
2904
3146
|
"account if you are new. Then check the code matches and press",
|
|
@@ -2959,6 +3201,10 @@ async function printDetection(d, facts) {
|
|
|
2959
3201
|
if (possible.length)
|
|
2960
3202
|
Me(possible.map((s) => `${import_picocolors4.default.yellow("?")} ${import_picocolors4.default.bold(s.label)} ${import_picocolors4.default.dim(`only ${s.evidence.join(", ")}`)}`).join(`
|
|
2961
3203
|
`), "Maybe in use, your agent will check"), await pace(motion, 150);
|
|
3204
|
+
let watch = watchList(d, facts);
|
|
3205
|
+
if (watch.length)
|
|
3206
|
+
Me(watchRows(watch).join(`
|
|
3207
|
+
`), "Worth watching for uptime"), await pace(motion, 150);
|
|
2962
3208
|
for (let gap of d.gaps)
|
|
2963
3209
|
M2.info(import_picocolors4.default.dim(gap));
|
|
2964
3210
|
}
|
|
@@ -2990,7 +3236,7 @@ function registerMcp(agent) {
|
|
|
2990
3236
|
return res.status === 0 || /already exists/i.test(`${res.stderr}${res.stdout}`);
|
|
2991
3237
|
}
|
|
2992
3238
|
async function init() {
|
|
2993
|
-
if (console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${
|
|
3239
|
+
if (console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), await horizon({ motion }), Ie(`${badge("LopeBase")} set up this project`), await noticeNewerCli(), Me(planLines().join(`
|
|
2994
3240
|
`), "Here is what happens"), !guard(await ye({
|
|
2995
3241
|
message: `Scan ${import_picocolors4.default.bold(cwd)}?`,
|
|
2996
3242
|
initialValue: !0
|
|
@@ -3015,16 +3261,44 @@ async function init() {
|
|
|
3015
3261
|
initialValue: facts.name?.replace(/^@[^/]+\//, "") ?? ""
|
|
3016
3262
|
})), chosen = guard(await fe({
|
|
3017
3263
|
message: "What should we connect? (space to toggle, enter to go on)",
|
|
3018
|
-
options:
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3264
|
+
options: [
|
|
3265
|
+
...detection.sources.map((src) => ({
|
|
3266
|
+
value: src.label,
|
|
3267
|
+
label: src.status === "found" ? src.label : src.status === "possible" ? `${src.label} ${import_picocolors4.default.dim("(maybe in use, we'll check)")}` : `${src.label} ${import_picocolors4.default.dim("(not set up yet, we'll help)")}`,
|
|
3268
|
+
hint: src.why
|
|
3269
|
+
})),
|
|
3270
|
+
...watchList(detection, facts).some((w) => w.how === "address") ? [
|
|
3271
|
+
{
|
|
3272
|
+
value: PAGEVIEWS,
|
|
3273
|
+
label: `${PAGEVIEWS} ${import_picocolors4.default.dim("(one tag, no cookies)")}`,
|
|
3274
|
+
hint: "LopeBase counts pages, sources, and visits itself."
|
|
3275
|
+
}
|
|
3276
|
+
] : []
|
|
3277
|
+
],
|
|
3023
3278
|
initialValues: detection.sources.filter((src) => src.status === "found" || src.source === "gsc" || src.source === "uptime").map((src) => src.label),
|
|
3024
3279
|
required: !1
|
|
3025
3280
|
}));
|
|
3026
3281
|
M2.step(stepTitle(4));
|
|
3027
|
-
let prompt = onboardingPrompt({
|
|
3282
|
+
let prompt = onboardingPrompt({
|
|
3283
|
+
facts,
|
|
3284
|
+
detection,
|
|
3285
|
+
chosen,
|
|
3286
|
+
productName,
|
|
3287
|
+
watch: watchList(detection, facts),
|
|
3288
|
+
pageviews: chosen.includes(PAGEVIEWS)
|
|
3289
|
+
});
|
|
3290
|
+
await handOff(prompt, (agent) => [
|
|
3291
|
+
`${import_picocolors4.default.bold(agent)} will now:`,
|
|
3292
|
+
` ${brand("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
|
|
3293
|
+
` ${brand("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
|
|
3294
|
+
` ${brand("3.")} pick what to watch for uptime: ${watchList(detection, facts).map((w) => w.label).join(", ") || "the site"}`,
|
|
3295
|
+
` ${brand("4.")} stop and ask you for any sign-in, key, or yes`,
|
|
3296
|
+
"",
|
|
3297
|
+
import_picocolors4.default.dim("You can follow along in your workspace as each one turns green.")
|
|
3298
|
+
]);
|
|
3299
|
+
}
|
|
3300
|
+
async function handOff(prompt, plan) {
|
|
3301
|
+
let agents = installedAgents();
|
|
3028
3302
|
if (!agents.length || flag("print")) {
|
|
3029
3303
|
if (!agents.length)
|
|
3030
3304
|
M2.info("No coding agent found on this computer (Claude Code, Codex, and others). Paste the prompt below into the one you use.");
|
|
@@ -3049,14 +3323,7 @@ async function init() {
|
|
|
3049
3323
|
s.stop(`Added the LopeBase tools to ${agent.label} for this folder.`);
|
|
3050
3324
|
else
|
|
3051
3325
|
s.stop(import_picocolors4.default.yellow(`Add an MCP server to ${agent.label} that runs: npx -y lopebase mcp`));
|
|
3052
|
-
Me(
|
|
3053
|
-
`${import_picocolors4.default.bold(agent.label)} will now:`,
|
|
3054
|
-
` ${import_picocolors4.default.dim("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
|
|
3055
|
-
` ${import_picocolors4.default.dim("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
|
|
3056
|
-
` ${import_picocolors4.default.dim("3.")} stop and ask you for any sign-in, key, or yes`,
|
|
3057
|
-
"",
|
|
3058
|
-
import_picocolors4.default.dim("You can follow along in your workspace as each one turns green.")
|
|
3059
|
-
].join(`
|
|
3326
|
+
Me(plan(agent.label).join(`
|
|
3060
3327
|
`), "Handing over"), Se(`Starting ${agent.label}. Press Ctrl+C any time to stop.`);
|
|
3061
3328
|
let child = spawnAgent(agent.bin, agent.args(prompt), {
|
|
3062
3329
|
stdio: "inherit",
|
|
@@ -3064,17 +3331,67 @@ async function init() {
|
|
|
3064
3331
|
});
|
|
3065
3332
|
child.on("error", (e) => bail(`Could not start ${agent.label}: ${e.message}`)), child.on("exit", (code) => process.exit(code ?? 0));
|
|
3066
3333
|
}
|
|
3334
|
+
async function noticeNewerCli() {
|
|
3335
|
+
let mine = version(), latest = await latestOnNpm("lopebase");
|
|
3336
|
+
if (mine && latest && isBehind(mine, latest))
|
|
3337
|
+
M2.warn(`lopebase ${latest} is out (this is ${mine}). Run ${brand("npx lopebase@latest")} for the newest setup steps.`);
|
|
3338
|
+
}
|
|
3339
|
+
async function update() {
|
|
3340
|
+
console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), await horizon({ motion }), Ie(`${badge("LopeBase")} update`), await noticeNewerCli();
|
|
3341
|
+
let s = Y2();
|
|
3342
|
+
s.start("Looking for the LopeBase Adapter in this project");
|
|
3343
|
+
let [installs, npmLatest] = await Promise.all([
|
|
3344
|
+
Promise.resolve(findAdapterInstalls(cwd)),
|
|
3345
|
+
latestOnNpm("@lopebase/adapter")
|
|
3346
|
+
]);
|
|
3347
|
+
if (await pace(motion, 300), !installs.length)
|
|
3348
|
+
return s.stop("No LopeBase Adapter in this project."), Me([
|
|
3349
|
+
"Connections, uptime checks, and pageviews update on their own in",
|
|
3350
|
+
"the console, so there is nothing to install for them.",
|
|
3351
|
+
"",
|
|
3352
|
+
`To add the adapter (your users, numbers, and service health), run ${brand("npx lopebase")}.`
|
|
3353
|
+
].join(`
|
|
3354
|
+
`), "Nothing to update here"), Se("Done.");
|
|
3355
|
+
let latest = npmLatest && isBehind(ADAPTER_VERSION, npmLatest) ? npmLatest : ADAPTER_VERSION, behind = installs.filter((i) => isBehind(runningVersion(i), latest));
|
|
3356
|
+
if (s.stop(installs.map((i) => `${i.dir === "." ? "This folder" : i.dir} runs ${import_picocolors4.default.bold(runningVersion(i))}`).join(", ")), !behind.length)
|
|
3357
|
+
return Se(`Up to date with ${import_picocolors4.default.bold(latest)}.`);
|
|
3358
|
+
let from = behind.map(runningVersion).sort((a, b) => isBehind(a, b) ? -1 : 1)[0], releases = releasesSince(from);
|
|
3359
|
+
if (Me(releases.flatMap((r) => [
|
|
3360
|
+
`${brand("◆")} ${import_picocolors4.default.bold(r.version)} ${import_picocolors4.default.dim(r.date)}`,
|
|
3361
|
+
...r.features.map((f) => ` ${f}`),
|
|
3362
|
+
...(r.breaking ?? []).map((b) => ` ${import_picocolors4.default.yellow("!")} ${b}`)
|
|
3363
|
+
]).join(`
|
|
3364
|
+
`), `New since ${from}`), npmLatest && isBehind(ADAPTER_VERSION, npmLatest))
|
|
3365
|
+
M2.warn(`npm has ${npmLatest}, newer than these notes. Run ${brand("npx lopebase@latest update")} for its steps.`);
|
|
3366
|
+
if (!guard(await ye({
|
|
3367
|
+
message: "Hand the update to your coding agent? It asks before each change.",
|
|
3368
|
+
initialValue: !0
|
|
3369
|
+
})))
|
|
3370
|
+
return Me(upgradePrompt(from), "Or paste this into your coding agent"), Se("Nothing was changed.");
|
|
3371
|
+
let where = behind.map((i) => i.dir).join(", ");
|
|
3372
|
+
await handOff(`${upgradePrompt(from)}
|
|
3373
|
+
|
|
3374
|
+
The adapter is in: ${where}. If you have the lopebase MCP tools, call get_onboarding_status afterwards and do any step it marks next.`, (agent) => [
|
|
3375
|
+
`${import_picocolors4.default.bold(agent)} will now:`,
|
|
3376
|
+
` ${brand("1.")} update @lopebase/adapter to ${latest} in ${where}`,
|
|
3377
|
+
` ${brand("2.")} make the changes the new version needs, asking you first`,
|
|
3378
|
+
` ${brand("3.")} run the adapter check and your tests`
|
|
3379
|
+
]);
|
|
3380
|
+
}
|
|
3067
3381
|
async function main() {
|
|
3068
3382
|
switch (command) {
|
|
3069
3383
|
case "init":
|
|
3070
3384
|
return init();
|
|
3385
|
+
case "update":
|
|
3386
|
+
case "upgrade":
|
|
3387
|
+
return update();
|
|
3071
3388
|
case "login":
|
|
3072
|
-
return await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${
|
|
3389
|
+
return await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${badge("LopeBase")} sign in`), await login(), Se("Ready. Run lopebase in any project.");
|
|
3073
3390
|
case "scan": {
|
|
3074
3391
|
let facts = scanProject(cwd), detection = detectStack(facts);
|
|
3075
3392
|
if (flag("json"))
|
|
3076
3393
|
return console.log(JSON.stringify({ facts, detection }, null, 2));
|
|
3077
|
-
return Ie(`${
|
|
3394
|
+
return Ie(`${badge("LopeBase")} scan`), await printDetection(detection, facts), Se(`Run ${import_picocolors4.default.cyan("npx lopebase")} to connect what it found.`);
|
|
3078
3395
|
}
|
|
3079
3396
|
case "prompt": {
|
|
3080
3397
|
let facts = scanProject(cwd), detection = detectStack(facts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lopebase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Set up LopeBase from your project's folder: scan your stack, connect your workspace, and let your coding agent do the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
|
-
"./detect": "./src/detect.ts"
|
|
11
|
+
"./detect": "./src/detect.ts",
|
|
12
|
+
"./releases": "./src/releases.ts"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist",
|