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 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 readFileSync3 } from "node:fs";
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 (!has("sentry")) {
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. Finish when get_onboarding_status says done. Summarize what is connected and anything left for later."
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), MARK = [
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")}${import_picocolors3.default.dim("Base")}${opts.version ? import_picocolors3.default.dim(` v${opts.version}`) : ""}`,
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.yellow(import_picocolors3.default.dim(row)) : import_picocolors3.default.yellow(row);
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.yellow(import_picocolors3.default.dim(row)) : import_picocolors3.default.yellow(row) : blank[i]), up = `\x1B[${MARK.length}A`, onInterrupt = () => {
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) => `${import_picocolors3.default.yellow(String(i + 1))} ${import_picocolors3.default.bold(s.title)}
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
- ${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} Set up LopeBase from this project's folder.
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
- ${import_picocolors4.default.cyan("https://lopebase.com")}
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(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
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(import_picocolors4.default.yellow(device.userCode))}`,
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(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} set up this project`), Me(planLines().join(`
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: detection.sources.map((src) => ({
3019
- value: src.label,
3020
- 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)")}`,
3021
- hint: src.why
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({ facts, detection, chosen, productName }), agents = installedAgents();
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(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} sign in`), await login(), Se("Ready. Run lopebase in any project.");
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(`${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} scan`), await printDetection(detection, facts), Se(`Run ${import_picocolors4.default.cyan("npx lopebase")} to connect what it found.`);
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.4.3",
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",