lopebase 0.5.0 → 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
@@ -83,6 +83,7 @@ Made for web products, and fine with anything else:
83
83
  | --- | --- |
84
84
  | `npx lopebase` | Scan, connect, choose, and hand over to your agent. |
85
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. |
86
87
  | `npx lopebase scan` | Show what the scan found. `--json` for scripts. |
87
88
  | `npx lopebase prompt` | Print the setup prompt instead of starting an agent. |
88
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;
@@ -2927,6 +3007,52 @@ function watchRows(items) {
2927
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")}`);
2928
3008
  }
2929
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
+ }
3054
+ }
3055
+
2930
3056
  // bin/lopebase.ts
2931
3057
  var [command = "init", ...rest] = process.argv.slice(2), HELP = `
2932
3058
  ${badge("LopeBase")} Set up LopeBase from this project's folder.
@@ -2934,6 +3060,7 @@ ${badge("LopeBase")} Set up LopeBase from this project's folder.
2934
3060
  ${import_picocolors4.default.bold("Usage")}
2935
3061
  npx lopebase scan, connect, and hand over to your agent
2936
3062
  npx lopebase login connect this computer (--paste for a token)
3063
+ npx lopebase update update this project's LopeBase Adapter to the latest
2937
3064
  npx lopebase scan [--json] show what the scan found
2938
3065
  npx lopebase prompt print the setup prompt instead
2939
3066
  npx lopebase mcp run the LopeBase tools as a stdio MCP server
@@ -2954,7 +3081,7 @@ ${import_picocolors4.default.bold("Learn more")}
2954
3081
  }, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.", PAGEVIEWS = "LopeBase pageviews";
2955
3082
  function version() {
2956
3083
  try {
2957
- 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;
2958
3085
  } catch {
2959
3086
  return;
2960
3087
  }
@@ -3109,7 +3236,7 @@ function registerMcp(agent) {
3109
3236
  return res.status === 0 || /already exists/i.test(`${res.stderr}${res.stdout}`);
3110
3237
  }
3111
3238
  async function init() {
3112
- if (console.clear(), await showLogo({ tagline: TAGLINE, version: version(), motion }), await horizon({ motion }), Ie(`${badge("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(`
3113
3240
  `), "Here is what happens"), !guard(await ye({
3114
3241
  message: `Scan ${import_picocolors4.default.bold(cwd)}?`,
3115
3242
  initialValue: !0
@@ -3159,7 +3286,19 @@ async function init() {
3159
3286
  productName,
3160
3287
  watch: watchList(detection, facts),
3161
3288
  pageviews: chosen.includes(PAGEVIEWS)
3162
- }), agents = installedAgents();
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();
3163
3302
  if (!agents.length || flag("print")) {
3164
3303
  if (!agents.length)
3165
3304
  M2.info("No coding agent found on this computer (Claude Code, Codex, and others). Paste the prompt below into the one you use.");
@@ -3184,15 +3323,7 @@ async function init() {
3184
3323
  s.stop(`Added the LopeBase tools to ${agent.label} for this folder.`);
3185
3324
  else
3186
3325
  s.stop(import_picocolors4.default.yellow(`Add an MCP server to ${agent.label} that runs: npx -y lopebase mcp`));
3187
- Me([
3188
- `${import_picocolors4.default.bold(agent.label)} will now:`,
3189
- ` ${brand("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
3190
- ` ${brand("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
3191
- ` ${brand("3.")} pick what to watch for uptime: ${watchList(detection, facts).map((w) => w.label).join(", ") || "the site"}`,
3192
- ` ${brand("4.")} stop and ask you for any sign-in, key, or yes`,
3193
- "",
3194
- import_picocolors4.default.dim("You can follow along in your workspace as each one turns green.")
3195
- ].join(`
3326
+ Me(plan(agent.label).join(`
3196
3327
  `), "Handing over"), Se(`Starting ${agent.label}. Press Ctrl+C any time to stop.`);
3197
3328
  let child = spawnAgent(agent.bin, agent.args(prompt), {
3198
3329
  stdio: "inherit",
@@ -3200,10 +3331,60 @@ async function init() {
3200
3331
  });
3201
3332
  child.on("error", (e) => bail(`Could not start ${agent.label}: ${e.message}`)), child.on("exit", (code) => process.exit(code ?? 0));
3202
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
+ }
3203
3381
  async function main() {
3204
3382
  switch (command) {
3205
3383
  case "init":
3206
3384
  return init();
3385
+ case "update":
3386
+ case "upgrade":
3387
+ return update();
3207
3388
  case "login":
3208
3389
  return await showLogo({ tagline: TAGLINE, version: version(), motion }), Ie(`${badge("LopeBase")} sign in`), await login(), Se("Ready. Run lopebase in any project.");
3209
3390
  case "scan": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopebase",
3
- "version": "0.5.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",