lopebase 0.1.0 → 0.3.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -20
  3. package/dist/lopebase.js +1718 -252
  4. package/package.json +20 -8
package/dist/lopebase.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __create = Object.create;
3
4
  var { getPrototypeOf: __getProtoOf, defineProperty: __defProp, getOwnPropertyNames: __getOwnPropNames } = Object;
4
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
@@ -13,7 +14,7 @@ var __toESMCache_node, __toESMCache_esm, __toESM = (mod, isNodeMode, target) =>
13
14
  return cached;
14
15
  }
15
16
  target = mod != null ? __create(__getProtoOf(mod)) : {};
16
- let to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target;
17
+ let to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target;
17
18
  if (mod && typeof mod === "object" || typeof mod === "function") {
18
19
  for (let key of __getOwnPropNames(mod))
19
20
  if (!__hasOwnProp.call(to, key))
@@ -27,6 +28,7 @@ var __toESMCache_node, __toESMCache_esm, __toESM = (mod, isNodeMode, target) =>
27
28
  return to;
28
29
  };
29
30
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
31
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
32
 
31
33
  // ../../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
32
34
  var require_src = __commonJS(function(exports, module) {
@@ -143,8 +145,364 @@ var require_picocolors = __commonJS(function(exports, module) {
143
145
  module.exports.createColors = createColors;
144
146
  });
145
147
 
148
+ // ../../node_modules/.bun/isexe@2.0.0/node_modules/isexe/windows.js
149
+ var require_windows = __commonJS(function(exports, module) {
150
+ module.exports = isexe;
151
+ isexe.sync = sync;
152
+ var fs = __require("fs");
153
+ function checkPathExt(path, options) {
154
+ var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
155
+ if (!pathext)
156
+ return !0;
157
+ if (pathext = pathext.split(";"), pathext.indexOf("") !== -1)
158
+ return !0;
159
+ for (var i = 0;i < pathext.length; i++) {
160
+ var p2 = pathext[i].toLowerCase();
161
+ if (p2 && path.substr(-p2.length).toLowerCase() === p2)
162
+ return !0;
163
+ }
164
+ return !1;
165
+ }
166
+ function checkStat(stat, path, options) {
167
+ if (!stat.isSymbolicLink() && !stat.isFile())
168
+ return !1;
169
+ return checkPathExt(path, options);
170
+ }
171
+ function isexe(path, options, cb) {
172
+ fs.stat(path, function(er, stat) {
173
+ cb(er, er ? !1 : checkStat(stat, path, options));
174
+ });
175
+ }
176
+ function sync(path, options) {
177
+ return checkStat(fs.statSync(path), path, options);
178
+ }
179
+ });
180
+
181
+ // ../../node_modules/.bun/isexe@2.0.0/node_modules/isexe/mode.js
182
+ var require_mode = __commonJS(function(exports, module) {
183
+ module.exports = isexe;
184
+ isexe.sync = sync;
185
+ var fs = __require("fs");
186
+ function isexe(path, options, cb) {
187
+ fs.stat(path, function(er, stat) {
188
+ cb(er, er ? !1 : checkStat(stat, options));
189
+ });
190
+ }
191
+ function sync(path, options) {
192
+ return checkStat(fs.statSync(path), options);
193
+ }
194
+ function checkStat(stat, options) {
195
+ return stat.isFile() && checkMode(stat, options);
196
+ }
197
+ function checkMode(stat, options) {
198
+ var { mode: mod, uid, gid } = stat, myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid(), myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid(), u2 = parseInt("100", 8), g2 = parseInt("010", 8), o2 = parseInt("001", 8), ug = u2 | g2, ret = mod & o2 || mod & g2 && gid === myGid || mod & u2 && uid === myUid || mod & ug && myUid === 0;
199
+ return ret;
200
+ }
201
+ });
202
+
203
+ // ../../node_modules/.bun/isexe@2.0.0/node_modules/isexe/index.js
204
+ var require_isexe = __commonJS(function(exports, module) {
205
+ var fs = __require("fs"), core;
206
+ if (process.platform === "win32" || global.TESTING_WINDOWS)
207
+ core = require_windows();
208
+ else
209
+ core = require_mode();
210
+ module.exports = isexe;
211
+ isexe.sync = sync;
212
+ function isexe(path, options, cb) {
213
+ if (typeof options === "function")
214
+ cb = options, options = {};
215
+ if (!cb) {
216
+ if (typeof Promise !== "function")
217
+ throw TypeError("callback not provided");
218
+ return new Promise(function(resolve, reject) {
219
+ isexe(path, options || {}, function(er, is) {
220
+ if (er)
221
+ reject(er);
222
+ else
223
+ resolve(is);
224
+ });
225
+ });
226
+ }
227
+ core(path, options || {}, function(er, is) {
228
+ if (er) {
229
+ if (er.code === "EACCES" || options && options.ignoreErrors)
230
+ er = null, is = !1;
231
+ }
232
+ cb(er, is);
233
+ });
234
+ }
235
+ function sync(path, options) {
236
+ try {
237
+ return core.sync(path, options || {});
238
+ } catch (er) {
239
+ if (options && options.ignoreErrors || er.code === "EACCES")
240
+ return !1;
241
+ else
242
+ throw er;
243
+ }
244
+ }
245
+ });
246
+
247
+ // ../../node_modules/.bun/which@2.0.2/node_modules/which/which.js
248
+ var require_which = __commonJS(function(exports, module) {
249
+ var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys", path = __require("path"), COLON = isWindows ? ";" : ":", isexe = require_isexe(), getNotFoundError = (cmd) => Object.assign(Error(`not found: ${cmd}`), { code: "ENOENT" }), getPathInfo = (cmd, opt) => {
250
+ let colon = opt.colon || COLON, pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [""] : [
251
+ ...isWindows ? [process.cwd()] : [],
252
+ ...(opt.path || process.env.PATH || "").split(colon)
253
+ ], pathExtExe = isWindows ? opt.pathExt || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM" : "", pathExt = isWindows ? pathExtExe.split(colon) : [""];
254
+ if (isWindows) {
255
+ if (cmd.indexOf(".") !== -1 && pathExt[0] !== "")
256
+ pathExt.unshift("");
257
+ }
258
+ return {
259
+ pathEnv,
260
+ pathExt,
261
+ pathExtExe
262
+ };
263
+ }, which = (cmd, opt, cb) => {
264
+ if (typeof opt === "function")
265
+ cb = opt, opt = {};
266
+ if (!opt)
267
+ opt = {};
268
+ let { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt), found = [], step = (i) => new Promise((resolve, reject) => {
269
+ if (i === pathEnv.length)
270
+ return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
271
+ let ppRaw = pathEnv[i], pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw, pCmd = path.join(pathPart, cmd), p2 = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
272
+ resolve(subStep(p2, i, 0));
273
+ }), subStep = (p2, i, ii) => new Promise((resolve, reject) => {
274
+ if (ii === pathExt.length)
275
+ return resolve(step(i + 1));
276
+ let ext = pathExt[ii];
277
+ isexe(p2 + ext, { pathExt: pathExtExe }, (er, is) => {
278
+ if (!er && is)
279
+ if (opt.all)
280
+ found.push(p2 + ext);
281
+ else
282
+ return resolve(p2 + ext);
283
+ return resolve(subStep(p2, i, ii + 1));
284
+ });
285
+ });
286
+ return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
287
+ }, whichSync = (cmd, opt) => {
288
+ opt = opt || {};
289
+ let { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt), found = [];
290
+ for (let i = 0;i < pathEnv.length; i++) {
291
+ let ppRaw = pathEnv[i], pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw, pCmd = path.join(pathPart, cmd), p2 = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
292
+ for (let j2 = 0;j2 < pathExt.length; j2++) {
293
+ let cur = p2 + pathExt[j2];
294
+ try {
295
+ if (isexe.sync(cur, { pathExt: pathExtExe }))
296
+ if (opt.all)
297
+ found.push(cur);
298
+ else
299
+ return cur;
300
+ } catch (ex) {}
301
+ }
302
+ }
303
+ if (opt.all && found.length)
304
+ return found;
305
+ if (opt.nothrow)
306
+ return null;
307
+ throw getNotFoundError(cmd);
308
+ };
309
+ module.exports = which;
310
+ which.sync = whichSync;
311
+ });
312
+
313
+ // ../../node_modules/.bun/path-key@3.1.1/node_modules/path-key/index.js
314
+ var require_path_key = __commonJS(function(exports, module) {
315
+ var pathKey = (options = {}) => {
316
+ let environment = options.env || process.env;
317
+ if ((options.platform || process.platform) !== "win32")
318
+ return "PATH";
319
+ return Object.keys(environment).reverse().find((key) => key.toUpperCase() === "PATH") || "Path";
320
+ };
321
+ module.exports = pathKey;
322
+ module.exports.default = pathKey;
323
+ });
324
+
325
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/resolveCommand.js
326
+ var require_resolveCommand = __commonJS(function(exports, module) {
327
+ var path = __require("path"), which = require_which(), getPathKey = require_path_key();
328
+ function resolveCommandAttempt(parsed, withoutPathExt) {
329
+ let env = parsed.options.env || process.env, cwd = process.cwd(), hasCustomCwd = parsed.options.cwd != null, shouldSwitchCwd = hasCustomCwd && process.chdir !== void 0 && !process.chdir.disabled;
330
+ if (shouldSwitchCwd)
331
+ try {
332
+ process.chdir(parsed.options.cwd);
333
+ } catch (err) {}
334
+ let resolved;
335
+ try {
336
+ resolved = which.sync(parsed.command, {
337
+ path: env[getPathKey({ env })],
338
+ pathExt: withoutPathExt ? path.delimiter : void 0
339
+ });
340
+ } catch (e2) {} finally {
341
+ if (shouldSwitchCwd)
342
+ process.chdir(cwd);
343
+ }
344
+ if (resolved)
345
+ resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
346
+ return resolved;
347
+ }
348
+ function resolveCommand(parsed) {
349
+ return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, !0);
350
+ }
351
+ module.exports = resolveCommand;
352
+ });
353
+
354
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/escape.js
355
+ var require_escape = __commonJS(function(exports, module) {
356
+ var metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
357
+ function escapeCommand(arg) {
358
+ return arg = arg.replace(metaCharsRegExp, "^$1"), arg;
359
+ }
360
+ function escapeArgument(arg, doubleEscapeMetaChars) {
361
+ if (arg = `${arg}`, arg = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\""), arg = arg.replace(/(?=(\\+?)?)\1$/, "$1$1"), arg = `"${arg}"`, arg = arg.replace(metaCharsRegExp, "^$1"), doubleEscapeMetaChars)
362
+ arg = arg.replace(metaCharsRegExp, "^$1");
363
+ return arg;
364
+ }
365
+ exports.command = escapeCommand;
366
+ exports.argument = escapeArgument;
367
+ });
368
+
369
+ // ../../node_modules/.bun/shebang-regex@3.0.0/node_modules/shebang-regex/index.js
370
+ var require_shebang_regex = __commonJS(function(exports, module) {
371
+ module.exports = /^#!(.*)/;
372
+ });
373
+
374
+ // ../../node_modules/.bun/shebang-command@2.0.0/node_modules/shebang-command/index.js
375
+ var require_shebang_command = __commonJS(function(exports, module) {
376
+ var shebangRegex = require_shebang_regex();
377
+ module.exports = (string = "") => {
378
+ let match = string.match(shebangRegex);
379
+ if (!match)
380
+ return null;
381
+ let [path, argument] = match[0].replace(/#! ?/, "").split(" "), binary = path.split("/").pop();
382
+ if (binary === "env")
383
+ return argument;
384
+ return argument ? `${binary} ${argument}` : binary;
385
+ };
386
+ });
387
+
388
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/readShebang.js
389
+ var require_readShebang = __commonJS(function(exports, module) {
390
+ var fs = __require("fs"), shebangCommand = require_shebang_command();
391
+ function readShebang(command) {
392
+ let buffer = Buffer.alloc(150), fd;
393
+ try {
394
+ fd = fs.openSync(command, "r"), fs.readSync(fd, buffer, 0, 150, 0), fs.closeSync(fd);
395
+ } catch (e2) {}
396
+ return shebangCommand(buffer.toString());
397
+ }
398
+ module.exports = readShebang;
399
+ });
400
+
401
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/lib/parse.js
402
+ var require_parse = __commonJS(function(exports, module) {
403
+ var path = __require("path"), resolveCommand = require_resolveCommand(), escape = require_escape(), readShebang = require_readShebang(), isWin = process.platform === "win32", isExecutableRegExp = /\.(?:com|exe)$/i, isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
404
+ function detectShebang(parsed) {
405
+ parsed.file = resolveCommand(parsed);
406
+ let shebang = parsed.file && readShebang(parsed.file);
407
+ if (shebang)
408
+ return parsed.args.unshift(parsed.file), parsed.command = shebang, resolveCommand(parsed);
409
+ return parsed.file;
410
+ }
411
+ function parseNonShell(parsed) {
412
+ if (!isWin)
413
+ return parsed;
414
+ let commandFile = detectShebang(parsed), needsShell = !isExecutableRegExp.test(commandFile);
415
+ if (parsed.options.forceShell || needsShell) {
416
+ let needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
417
+ parsed.command = path.normalize(parsed.command), parsed.command = escape.command(parsed.command), parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
418
+ let shellCommand = [parsed.command].concat(parsed.args).join(" ");
419
+ parsed.args = ["/d", "/s", "/c", `"${shellCommand}"`], parsed.command = process.env.comspec || "cmd.exe", parsed.options.windowsVerbatimArguments = !0;
420
+ }
421
+ return parsed;
422
+ }
423
+ function parse(command, args, options) {
424
+ if (args && !Array.isArray(args))
425
+ options = args, args = null;
426
+ args = args ? args.slice(0) : [], options = Object.assign({}, options);
427
+ let parsed = {
428
+ command,
429
+ args,
430
+ options,
431
+ file: void 0,
432
+ original: {
433
+ command,
434
+ args
435
+ }
436
+ };
437
+ return options.shell ? parsed : parseNonShell(parsed);
438
+ }
439
+ module.exports = parse;
440
+ });
441
+
442
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/lib/enoent.js
443
+ var require_enoent = __commonJS(function(exports, module) {
444
+ var isWin = process.platform === "win32";
445
+ function notFoundError(original, syscall) {
446
+ return Object.assign(Error(`${syscall} ${original.command} ENOENT`), {
447
+ code: "ENOENT",
448
+ errno: "ENOENT",
449
+ syscall: `${syscall} ${original.command}`,
450
+ path: original.command,
451
+ spawnargs: original.args
452
+ });
453
+ }
454
+ function hookChildProcess(cp, parsed) {
455
+ if (!isWin)
456
+ return;
457
+ let originalEmit = cp.emit;
458
+ cp.emit = function(name, arg1) {
459
+ if (name === "exit") {
460
+ let err = verifyENOENT(arg1, parsed);
461
+ if (err)
462
+ return originalEmit.call(cp, "error", err);
463
+ }
464
+ return originalEmit.apply(cp, arguments);
465
+ };
466
+ }
467
+ function verifyENOENT(status, parsed) {
468
+ if (isWin && status === 1 && !parsed.file)
469
+ return notFoundError(parsed.original, "spawn");
470
+ return null;
471
+ }
472
+ function verifyENOENTSync(status, parsed) {
473
+ if (isWin && status === 1 && !parsed.file)
474
+ return notFoundError(parsed.original, "spawnSync");
475
+ return null;
476
+ }
477
+ module.exports = {
478
+ hookChildProcess,
479
+ verifyENOENT,
480
+ verifyENOENTSync,
481
+ notFoundError
482
+ };
483
+ });
484
+
485
+ // ../../node_modules/.bun/cross-spawn@7.0.6/node_modules/cross-spawn/index.js
486
+ var require_cross_spawn = __commonJS(function(exports, module) {
487
+ var cp = __require("child_process"), parse = require_parse(), enoent = require_enoent();
488
+ function spawn(command, args, options) {
489
+ let parsed = parse(command, args, options), spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
490
+ return enoent.hookChildProcess(spawned, parsed), spawned;
491
+ }
492
+ function spawnSync(command, args, options) {
493
+ let parsed = parse(command, args, options), result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
494
+ return result.error = result.error || enoent.verifyENOENTSync(result.status, parsed), result;
495
+ }
496
+ module.exports = spawn;
497
+ module.exports.spawn = spawn;
498
+ module.exports.sync = spawnSync;
499
+ module.exports._parse = parse;
500
+ module.exports._enoent = enoent;
501
+ });
502
+
146
503
  // bin/lopebase.ts
147
- import { spawn, spawnSync as spawnSync2 } from "node:child_process";
504
+ import { spawn as spawn2 } from "node:child_process";
505
+ import { readFileSync as readFileSync3 } from "node:fs";
148
506
 
149
507
  // ../../node_modules/.bun/@clack+prompts@0.11.0/node_modules/@clack/prompts/dist/index.mjs
150
508
  import { stripVTControlCharacters as S2 } from "node:util";
@@ -592,17 +950,17 @@ var V2 = ce(), u = (t, n) => V2 ? t : n, le = u("◆", "*"), L2 = u("■", "x"),
592
950
  return import_picocolors2.default.green(C);
593
951
  }
594
952
  }, G2 = (t) => {
595
- let { cursor: n, options: r, style: i } = t, s = t.maxItems ?? Number.POSITIVE_INFINITY, c = Math.max(process.stdout.rows - 4, 0), a = Math.min(c, Math.max(s, 5)), l = 0;
596
- n >= l + a - 3 ? l = Math.max(Math.min(n - a + 3, r.length - a), 0) : n < l + 2 && (l = Math.max(n - 2, 0));
597
- let $ = a < r.length && l > 0, g = a < r.length && l + a < r.length;
598
- return r.slice(l, l + a).map((p, v, f) => {
599
- let j = v === 0 && $, E = v === f.length - 1 && g;
600
- return j || E ? import_picocolors2.default.dim("...") : i(p, v + l === n);
953
+ let { cursor: n, options: r2, style: i } = t, s = t.maxItems ?? Number.POSITIVE_INFINITY, c = Math.max(process.stdout.rows - 4, 0), a = Math.min(c, Math.max(s, 5)), l2 = 0;
954
+ n >= l2 + a - 3 ? l2 = Math.max(Math.min(n - a + 3, r2.length - a), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
955
+ let $2 = a < r2.length && l2 > 0, g2 = a < r2.length && l2 + a < r2.length;
956
+ return r2.slice(l2, l2 + a).map((p2, v2, f) => {
957
+ let j2 = v2 === 0 && $2, E = v2 === f.length - 1 && g2;
958
+ return j2 || E ? import_picocolors2.default.dim("...") : i(p2, v2 + l2 === n);
601
959
  });
602
960
  }, he = (t) => new RD({ validate: t.validate, placeholder: t.placeholder, defaultValue: t.defaultValue, initialValue: t.initialValue, render() {
603
961
  let n = `${import_picocolors2.default.gray(o)}
604
962
  ${b2(this.state)} ${t.message}
605
- `, r = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), i = this.value ? this.valueWithCursor : r;
963
+ `, r2 = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), i = this.value ? this.valueWithCursor : r2;
606
964
  switch (this.state) {
607
965
  case "error":
608
966
  return `${n.trim()}
@@ -622,7 +980,7 @@ ${import_picocolors2.default.cyan(d2)}
622
980
  } }).prompt(), ge = (t) => new MD({ validate: t.validate, mask: t.mask ?? $e, render() {
623
981
  let n = `${import_picocolors2.default.gray(o)}
624
982
  ${b2(this.state)} ${t.message}
625
- `, r = this.valueWithCursor, i = this.masked;
983
+ `, r2 = this.valueWithCursor, i = this.masked;
626
984
  switch (this.state) {
627
985
  case "error":
628
986
  return `${n.trim()}
@@ -635,19 +993,19 @@ ${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(th
635
993
  return `${n}${import_picocolors2.default.gray(o)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(i ?? ""))}${i ? `
636
994
  ${import_picocolors2.default.gray(o)}` : ""}`;
637
995
  default:
638
- return `${n}${import_picocolors2.default.cyan(o)} ${r}
996
+ return `${n}${import_picocolors2.default.cyan(o)} ${r2}
639
997
  ${import_picocolors2.default.cyan(d2)}
640
998
  `;
641
999
  }
642
1000
  } }).prompt();
643
1001
  var ve = (t) => {
644
- let n = (r, i) => {
645
- let s = r.label ?? String(r.value);
1002
+ let n = (r2, i) => {
1003
+ let s = r2.label ?? String(r2.value);
646
1004
  switch (i) {
647
1005
  case "selected":
648
1006
  return `${import_picocolors2.default.dim(s)}`;
649
1007
  case "active":
650
- return `${import_picocolors2.default.green(k2)} ${s} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}`;
1008
+ return `${import_picocolors2.default.green(k2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}`;
651
1009
  case "cancelled":
652
1010
  return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}`;
653
1011
  default:
@@ -655,17 +1013,17 @@ var ve = (t) => {
655
1013
  }
656
1014
  };
657
1015
  return new LD({ options: t.options, initialValue: t.initialValue, render() {
658
- let r = `${import_picocolors2.default.gray(o)}
1016
+ let r2 = `${import_picocolors2.default.gray(o)}
659
1017
  ${b2(this.state)} ${t.message}
660
1018
  `;
661
1019
  switch (this.state) {
662
1020
  case "submit":
663
- return `${r}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "selected")}`;
1021
+ return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "selected")}`;
664
1022
  case "cancel":
665
- return `${r}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "cancelled")}
1023
+ return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "cancelled")}
666
1024
  ${import_picocolors2.default.gray(o)}`;
667
1025
  default:
668
- return `${r}${import_picocolors2.default.cyan(o)} ${G2({ cursor: this.cursor, options: this.options, maxItems: t.maxItems, style: (i, s) => n(i, s ? "active" : "inactive") }).join(`
1026
+ return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ cursor: this.cursor, options: this.options, maxItems: t.maxItems, style: (i, s) => n(i, s ? "active" : "inactive") }).join(`
669
1027
  ${import_picocolors2.default.cyan(o)} `)}
670
1028
  ${import_picocolors2.default.cyan(d2)}
671
1029
  `;
@@ -673,16 +1031,16 @@ ${import_picocolors2.default.cyan(d2)}
673
1031
  } }).prompt();
674
1032
  };
675
1033
  var fe = (t) => {
676
- let n = (r, i) => {
677
- let s = r.label ?? String(r.value);
678
- return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
1034
+ let n = (r2, i) => {
1035
+ let s = r2.label ?? String(r2.value);
1036
+ return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
679
1037
  };
680
- return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? !0, cursorAt: t.cursorAt, validate(r) {
681
- if (this.required && r.length === 0)
1038
+ return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? !0, cursorAt: t.cursorAt, validate(r2) {
1039
+ if (this.required && r2.length === 0)
682
1040
  return `Please select at least one option.
683
1041
  ${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
684
1042
  }, render() {
685
- let r = `${import_picocolors2.default.gray(o)}
1043
+ let r2 = `${import_picocolors2.default.gray(o)}
686
1044
  ${b2(this.state)} ${t.message}
687
1045
  `, i = (s, c) => {
688
1046
  let a = this.value.includes(s.value);
@@ -690,23 +1048,23 @@ ${b2(this.state)} ${t.message}
690
1048
  };
691
1049
  switch (this.state) {
692
1050
  case "submit":
693
- return `${r}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
1051
+ return `${r2}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
694
1052
  case "cancel": {
695
1053
  let s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => n(c, "cancelled")).join(import_picocolors2.default.dim(", "));
696
- return `${r}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
1054
+ return `${r2}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
697
1055
  ${import_picocolors2.default.gray(o)}` : ""}`;
698
1056
  }
699
1057
  case "error": {
700
1058
  let s = this.error.split(`
701
1059
  `).map((c, a) => a === 0 ? `${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(c)}` : ` ${c}`).join(`
702
1060
  `);
703
- return `${r + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
1061
+ return `${r2 + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
704
1062
  ${import_picocolors2.default.yellow(o)} `)}
705
1063
  ${s}
706
1064
  `;
707
1065
  }
708
1066
  default:
709
- return `${r}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
1067
+ return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
710
1068
  ${import_picocolors2.default.cyan(o)} `)}
711
1069
  ${import_picocolors2.default.cyan(d2)}
712
1070
  `;
@@ -714,13 +1072,13 @@ ${import_picocolors2.default.cyan(d2)}
714
1072
  } }).prompt();
715
1073
  };
716
1074
  var Me = (t = "", n = "") => {
717
- let r = `
1075
+ let r2 = `
718
1076
  ${t}
719
1077
  `.split(`
720
- `), i = S2(n).length, s = Math.max(r.reduce((a, l) => {
721
- let $ = S2(l);
722
- return $.length > a ? $.length : a;
723
- }, 0), i) + 2, c = r.map((a) => `${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(a)}${" ".repeat(s - S2(a).length)}${import_picocolors2.default.gray(o)}`).join(`
1078
+ `), i = S2(n).length, s = Math.max(r2.reduce((a, l2) => {
1079
+ let $2 = S2(l2);
1080
+ return $2.length > a ? $2.length : a;
1081
+ }, 0), i) + 2, c = r2.map((a) => `${import_picocolors2.default.gray(o)} ${import_picocolors2.default.dim(a)}${" ".repeat(s - S2(a).length)}${import_picocolors2.default.gray(o)}`).join(`
724
1082
  `);
725
1083
  process.stdout.write(`${import_picocolors2.default.gray(o)}
726
1084
  ${import_picocolors2.default.green(C)} ${import_picocolors2.default.reset(n)} ${import_picocolors2.default.gray(_2.repeat(Math.max(s - i - 1, 1)) + me)}
@@ -740,13 +1098,13 @@ ${import_picocolors2.default.gray(d2)} ${t}
740
1098
 
741
1099
  `);
742
1100
  }, M2 = { message: (t = "", { symbol: n = import_picocolors2.default.gray(o) } = {}) => {
743
- let r = [`${import_picocolors2.default.gray(o)}`];
1101
+ let r2 = [`${import_picocolors2.default.gray(o)}`];
744
1102
  if (t) {
745
1103
  let [i, ...s] = t.split(`
746
1104
  `);
747
- r.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors2.default.gray(o)} ${c}`));
1105
+ r2.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors2.default.gray(o)} ${c}`));
748
1106
  }
749
- process.stdout.write(`${r.join(`
1107
+ process.stdout.write(`${r2.join(`
750
1108
  `)}
751
1109
  `);
752
1110
  }, info: (t) => {
@@ -763,66 +1121,341 @@ ${import_picocolors2.default.gray(d2)} ${t}
763
1121
  M2.message(t, { symbol: import_picocolors2.default.red(K2) });
764
1122
  } }, J2 = `${import_picocolors2.default.gray(o)} `;
765
1123
  var Y2 = ({ indicator: t = "dots" } = {}) => {
766
- let n = V2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"], r = V2 ? 80 : 120, i = process.env.CI === "true", s, c, a = !1, l = "", $, g = performance.now(), p = (m) => {
767
- let h = m > 1 ? "Something went wrong" : "Canceled";
768
- a && N(h, m);
769
- }, v = () => p(2), f = () => p(1), j = () => {
770
- process.on("uncaughtExceptionMonitor", v), process.on("unhandledRejection", v), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p);
1124
+ let n = V2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"], r2 = V2 ? 80 : 120, i = process.env.CI === "true", s, c, a = !1, l2 = "", $2, g2 = performance.now(), p2 = (m2) => {
1125
+ let h2 = m2 > 1 ? "Something went wrong" : "Canceled";
1126
+ a && N2(h2, m2);
1127
+ }, v2 = () => p2(2), f = () => p2(1), j2 = () => {
1128
+ process.on("uncaughtExceptionMonitor", v2), process.on("unhandledRejection", v2), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p2);
771
1129
  }, E = () => {
772
- process.removeListener("uncaughtExceptionMonitor", v), process.removeListener("unhandledRejection", v), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p);
773
- }, B = () => {
774
- if ($ === void 0)
1130
+ process.removeListener("uncaughtExceptionMonitor", v2), process.removeListener("unhandledRejection", v2), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p2);
1131
+ }, B2 = () => {
1132
+ if ($2 === void 0)
775
1133
  return;
776
1134
  i && process.stdout.write(`
777
1135
  `);
778
- let m = $.split(`
1136
+ let m2 = $2.split(`
779
1137
  `);
780
- process.stdout.write(import_sisteransi2.cursor.move(-999, m.length - 1)), process.stdout.write(import_sisteransi2.erase.down(m.length));
781
- }, R = (m) => m.replace(/\.+$/, ""), O = (m) => {
782
- let h = (performance.now() - m) / 1000, w = Math.floor(h / 60), I = Math.floor(h % 60);
783
- return w > 0 ? `[${w}m ${I}s]` : `[${I}s]`;
784
- }, H = (m = "") => {
785
- a = !0, s = fD(), l = R(m), g = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
1138
+ process.stdout.write(import_sisteransi2.cursor.move(-999, m2.length - 1)), process.stdout.write(import_sisteransi2.erase.down(m2.length));
1139
+ }, R2 = (m2) => m2.replace(/\.+$/, ""), O2 = (m2) => {
1140
+ let h2 = (performance.now() - m2) / 1000, w2 = Math.floor(h2 / 60), I2 = Math.floor(h2 % 60);
1141
+ return w2 > 0 ? `[${w2}m ${I2}s]` : `[${I2}s]`;
1142
+ }, H2 = (m2 = "") => {
1143
+ a = !0, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
786
1144
  `);
787
- let h = 0, w = 0;
788
- j(), c = setInterval(() => {
789
- if (i && l === $)
1145
+ let h2 = 0, w2 = 0;
1146
+ j2(), c = setInterval(() => {
1147
+ if (i && l2 === $2)
790
1148
  return;
791
- B(), $ = l;
792
- let I = import_picocolors2.default.magenta(n[h]);
1149
+ B2(), $2 = l2;
1150
+ let I2 = import_picocolors2.default.magenta(n[h2]);
793
1151
  if (i)
794
- process.stdout.write(`${I} ${l}...`);
1152
+ process.stdout.write(`${I2} ${l2}...`);
795
1153
  else if (t === "timer")
796
- process.stdout.write(`${I} ${l} ${O(g)}`);
1154
+ process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
797
1155
  else {
798
- let z = ".".repeat(Math.floor(w)).slice(0, 3);
799
- process.stdout.write(`${I} ${l}${z}`);
1156
+ let z2 = ".".repeat(Math.floor(w2)).slice(0, 3);
1157
+ process.stdout.write(`${I2} ${l2}${z2}`);
800
1158
  }
801
- h = h + 1 < n.length ? h + 1 : 0, w = w < n.length ? w + 0.125 : 0;
802
- }, r);
803
- }, N = (m = "", h = 0) => {
804
- a = !1, clearInterval(c), B();
805
- let w = h === 0 ? import_picocolors2.default.green(C) : h === 1 ? import_picocolors2.default.red(L2) : import_picocolors2.default.red(W2);
806
- l = R(m ?? l), t === "timer" ? process.stdout.write(`${w} ${l} ${O(g)}
807
- `) : process.stdout.write(`${w} ${l}
1159
+ h2 = h2 + 1 < n.length ? h2 + 1 : 0, w2 = w2 < n.length ? w2 + 0.125 : 0;
1160
+ }, r2);
1161
+ }, N2 = (m2 = "", h2 = 0) => {
1162
+ a = !1, clearInterval(c), B2();
1163
+ let w2 = h2 === 0 ? import_picocolors2.default.green(C) : h2 === 1 ? import_picocolors2.default.red(L2) : import_picocolors2.default.red(W2);
1164
+ l2 = R2(m2 ?? l2), t === "timer" ? process.stdout.write(`${w2} ${l2} ${O2(g2)}
1165
+ `) : process.stdout.write(`${w2} ${l2}
808
1166
  `), E(), s();
809
1167
  };
810
- return { start: H, stop: N, message: (m = "") => {
811
- l = R(m ?? l);
1168
+ return { start: H2, stop: N2, message: (m2 = "") => {
1169
+ l2 = R2(m2 ?? l2);
812
1170
  } };
813
1171
  };
814
1172
 
1173
+ // ../admin-adapter/src/security.ts
1174
+ var SIGNATURE_HEADER = "lopebase-signature";
1175
+ var enc = /* @__PURE__ */ new TextEncoder;
1176
+ function hex(bytes) {
1177
+ return [...new Uint8Array(bytes)].map((b3) => b3.toString(16).padStart(2, "0")).join("");
1178
+ }
1179
+ async function sha256Hex(data) {
1180
+ return hex(await crypto.subtle.digest("SHA-256", data));
1181
+ }
1182
+ async function hmacHex(secret, message) {
1183
+ let key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, !1, ["sign"]);
1184
+ return hex(await crypto.subtle.sign("HMAC", key, enc.encode(message)));
1185
+ }
1186
+ function newSigningSecret() {
1187
+ let bytes = crypto.getRandomValues(new Uint8Array(32)), bin = "";
1188
+ for (let b3 of bytes)
1189
+ bin += String.fromCharCode(b3);
1190
+ return "lbs_" + btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1191
+ }
1192
+ function payload(t, method, pathAndQuery, bodyHash) {
1193
+ return `${t}.${method.toUpperCase()}.${pathAndQuery}.${bodyHash}`;
1194
+ }
1195
+ async function signRequest(input) {
1196
+ let t = input.timestamp ?? Math.floor(Date.now() / 1000), body = typeof input.body === "string" ? enc.encode(input.body) : input.body ?? new Uint8Array, sig = await hmacHex(input.secret, payload(t, input.method, input.pathAndQuery, await sha256Hex(body)));
1197
+ return `t=${t},v1=${sig}`;
1198
+ }
1199
+ var SENSITIVE_PARTS = /* @__PURE__ */ new Set([
1200
+ "password",
1201
+ "passwd",
1202
+ "passcode",
1203
+ "pwd",
1204
+ "hash",
1205
+ "hashed",
1206
+ "salt",
1207
+ "secret",
1208
+ "secrets",
1209
+ "token",
1210
+ "tokens",
1211
+ "credential",
1212
+ "credentials",
1213
+ "ssn",
1214
+ "card",
1215
+ "cvv",
1216
+ "cvc",
1217
+ "iban",
1218
+ "otp",
1219
+ "mfa",
1220
+ "totp",
1221
+ "session",
1222
+ "cookie"
1223
+ ]), SENSITIVE_JOINED = /apikey|privatekey|secretkey|accesskey|accountnumber|routingnumber|stripecustomer|paymentmethod|recoverycode/;
1224
+ function isSensitiveFieldName(name) {
1225
+ let parts = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1226
+ if (parts.some((p2) => SENSITIVE_PARTS.has(p2)))
1227
+ return !0;
1228
+ return SENSITIVE_JOINED.test(parts.join(""));
1229
+ }
1230
+ var PERSON_WORDS = "user|customer|client|member|contact|person|people|owner|author|sender|recipient|requester|assignee|reporter|buyer|guest|subscriber|lead|employee|student|patient|attendee|signer|holder", PERSON_NAME = new RegExp(`(${PERSON_WORDS})s?name$`), PEOPLE_RESOURCE = new RegExp(`^(${PERSON_WORDS})s?$`);
1231
+
1232
+ // ../admin-adapter/src/adapter.ts
1233
+ var DEFAULT_BASE_PATH = "/api/lopebase";
1234
+ // ../admin-adapter/src/conformance.ts
1235
+ import { strict as assert } from "node:assert";
1236
+ async function body(res) {
1237
+ return await res.json();
1238
+ }
1239
+ function conformanceChecks(opts) {
1240
+ if (!opts.adapter && !opts.baseUrl)
1241
+ throw Error("conformanceChecks needs an adapter or a baseUrl");
1242
+ let remote = !opts.adapter, base = opts.baseUrl ? opts.baseUrl.replace(/\/$/, "") : `${opts.origin ?? "https://property.test"}${(opts.basePath ?? opts.adapter?.config.basePath ?? DEFAULT_BASE_PATH).replace(/\/$/, "")}`, send = (req) => opts.adapter ? opts.adapter.handle(req) : fetch(req);
1243
+ async function signedHeaders(url, method, bodyText, timestamp) {
1244
+ if (opts.secret) {
1245
+ let u2 = new URL(url);
1246
+ return {
1247
+ [SIGNATURE_HEADER]: await signRequest({
1248
+ secret: opts.secret,
1249
+ method,
1250
+ pathAndQuery: u2.pathname + u2.search,
1251
+ body: bodyText,
1252
+ timestamp
1253
+ })
1254
+ };
1255
+ }
1256
+ return opts.token ? { authorization: `Bearer ${opts.token}` } : {};
1257
+ }
1258
+ let authed = async (path, init) => {
1259
+ let url = base + path, method = init?.method ?? "GET", bodyText = typeof init?.body === "string" ? init.body : void 0;
1260
+ return send(new Request(url, {
1261
+ ...init,
1262
+ headers: {
1263
+ "content-type": "application/json",
1264
+ ...await signedHeaders(url, method, bodyText),
1265
+ ...init?.headers
1266
+ }
1267
+ }));
1268
+ }, resources = () => authed("/resources").then((r2) => body(r2)), checks = [
1269
+ {
1270
+ name: "GET /health returns { ok } without auth",
1271
+ async run() {
1272
+ let res = await send(new Request(`${base}/health`));
1273
+ assert.equal(res.status, 200);
1274
+ let b3 = await body(res);
1275
+ assert.equal(typeof b3.ok, "boolean");
1276
+ }
1277
+ },
1278
+ {
1279
+ name: "requests without a valid token get 401",
1280
+ async run() {
1281
+ let res = await send(new Request(`${base}/resources`));
1282
+ assert.equal(res.status, 401);
1283
+ }
1284
+ },
1285
+ {
1286
+ name: "GET /resources returns a catalog with field metadata",
1287
+ async run() {
1288
+ let res = await authed("/resources");
1289
+ assert.equal(res.status, 200);
1290
+ let b3 = await body(res);
1291
+ assert.ok(Array.isArray(b3.resources));
1292
+ for (let r2 of b3.resources)
1293
+ assert.ok(r2.name && r2.label && r2.readCapability), assert.ok(Array.isArray(r2.fields));
1294
+ }
1295
+ },
1296
+ {
1297
+ name: "GET /actions returns a catalog with param specs",
1298
+ async run() {
1299
+ let res = await authed("/actions");
1300
+ assert.equal(res.status, 200);
1301
+ let b3 = await body(res);
1302
+ assert.ok(Array.isArray(b3.actions));
1303
+ for (let a of b3.actions)
1304
+ assert.ok(a.name && a.label && a.capability), assert.ok(Array.isArray(a.params));
1305
+ }
1306
+ },
1307
+ {
1308
+ name: "GET /resources/:name paginates ({ rows, total? })",
1309
+ async run() {
1310
+ let cat = await resources(), name = opts.sampleResource ?? cat.resources[0]?.name;
1311
+ if (!name)
1312
+ return;
1313
+ let res = await authed(`/resources/${name}?limit=5`);
1314
+ assert.equal(res.status, 200);
1315
+ let page = await body(res);
1316
+ assert.ok(Array.isArray(page.rows));
1317
+ }
1318
+ },
1319
+ {
1320
+ name: "GET /resources/:name/:id returns a row or 404-null",
1321
+ async run() {
1322
+ let cat = await resources(), meta = opts.sampleResource ? cat.resources.find((r2) => r2.name === opts.sampleResource) : cat.resources[0];
1323
+ if (!meta)
1324
+ return;
1325
+ let first = (await body(await authed(`/resources/${meta.name}?limit=1`))).rows[0];
1326
+ if (!first)
1327
+ return;
1328
+ let idField = meta.fields[0]?.name ?? "id", res = await authed(`/resources/${meta.name}/${encodeURIComponent(String(first[idField]))}`);
1329
+ assert.ok(res.status === 200 || res.status === 404);
1330
+ }
1331
+ },
1332
+ {
1333
+ name: "GET /audit returns { rows: AuditEvent[] }",
1334
+ async run() {
1335
+ let res = await authed("/audit");
1336
+ assert.equal(res.status, 200);
1337
+ let b3 = await body(res);
1338
+ assert.ok(Array.isArray(b3.rows));
1339
+ }
1340
+ },
1341
+ {
1342
+ name: "an unknown route returns 404",
1343
+ async run() {
1344
+ let res = await authed("/does-not-exist");
1345
+ assert.equal(res.status, 404);
1346
+ }
1347
+ },
1348
+ {
1349
+ name: "no field in the catalog looks like a secret unless marked safe",
1350
+ async run() {
1351
+ let cat = await resources();
1352
+ for (let r2 of cat.resources)
1353
+ for (let f of r2.fields)
1354
+ assert.ok(!isSensitiveFieldName(f.name) || f.sensitive === "allow", `${r2.name}.${f.name} looks sensitive`);
1355
+ }
1356
+ },
1357
+ {
1358
+ name: "rows contain only declared fields, and a page is at most 100",
1359
+ async run() {
1360
+ let cat = await resources();
1361
+ for (let r2 of cat.resources) {
1362
+ let res = await authed(`/resources/${r2.name}?limit=1000`);
1363
+ assert.equal(res.status, 200);
1364
+ let page = await body(res);
1365
+ assert.ok(page.rows.length <= 100, `${r2.name} returned ${page.rows.length} rows`);
1366
+ let declared = /* @__PURE__ */ new Set(["id", ...r2.fields.map((f) => f.name)]);
1367
+ for (let row of page.rows)
1368
+ for (let k3 of Object.keys(row))
1369
+ assert.ok(declared.has(k3), `${r2.name} returned undeclared field ${k3}`);
1370
+ }
1371
+ }
1372
+ },
1373
+ ...opts.secret ? [
1374
+ {
1375
+ name: "a stale or tampered signature gets 401",
1376
+ async run() {
1377
+ let url = `${base}/resources`, stale = await signedHeaders(url, "GET", void 0, Math.floor(Date.now() / 1000) - 3600);
1378
+ assert.equal((await send(new Request(url, { headers: stale }))).status, 401);
1379
+ let good = await signedHeaders(url, "GET", void 0), tampered = `${url}?limit=1`;
1380
+ assert.equal((await send(new Request(tampered, { headers: good }))).status, 401);
1381
+ }
1382
+ }
1383
+ ] : [],
1384
+ {
1385
+ name: "a mutation writes an audit row (PATCH round-trips into /audit)",
1386
+ async run() {
1387
+ let writable = (await resources()).resources.find((r2) => r2.writeCapability && r2.fields.some((f) => f.editable));
1388
+ if (!writable || remote)
1389
+ return;
1390
+ let row = (await body(await authed(`/resources/${writable.name}?limit=1`))).rows[0];
1391
+ if (!row)
1392
+ return;
1393
+ let idField = writable.fields[0]?.name ?? "id", editField = writable.fields.find((f) => f.editable)?.name;
1394
+ if (!editField)
1395
+ return;
1396
+ let before = await body(await authed("/audit")), res = await authed(`/resources/${writable.name}/${encodeURIComponent(String(row[idField]))}`, {
1397
+ method: "PATCH",
1398
+ body: JSON.stringify({ patch: { [editField]: row[editField] } })
1399
+ });
1400
+ assert.equal(res.status, 200);
1401
+ let after = await body(await authed("/audit"));
1402
+ assert.ok(after.rows.length > before.rows.length, "expected a new audit row");
1403
+ }
1404
+ }
1405
+ ];
1406
+ return remote ? checks.filter((c) => !c.name.startsWith("a mutation writes")) : checks;
1407
+ }
815
1408
  // bin/lopebase.ts
816
- var import_picocolors3 = __toESM(require_picocolors(), 1);
1409
+ var import_picocolors4 = __toESM(require_picocolors(), 1);
1410
+
1411
+ // src/adapter-check.ts
1412
+ async function checkAdapter(input) {
1413
+ let log = input.log ?? (() => {}), failed = [], passed = 0, base = input.baseUrl.replace(/\/$/, ""), url = new URL(`${base}/resources`), status;
1414
+ try {
1415
+ let headers = input.secret ? {
1416
+ [SIGNATURE_HEADER]: await signRequest({
1417
+ secret: input.secret,
1418
+ method: "GET",
1419
+ pathAndQuery: url.pathname + url.search
1420
+ })
1421
+ } : { authorization: `Bearer ${input.token ?? ""}` };
1422
+ status = (await fetch(url, { headers })).status;
1423
+ } catch (e2) {
1424
+ let error = `Could not reach ${base} (${e2 instanceof Error ? e2.message : String(e2)}).`;
1425
+ return log(`FAIL ${error}`), { passed, failed: [{ name: "reachable", error }] };
1426
+ }
1427
+ if (status === 401) {
1428
+ let error = input.secret ? "The adapter refused the signature. The secret here does not match the app's LOPEBASE_SIGNING_SECRET, or the two clocks are more than 5 minutes apart." : "The adapter refused the token.";
1429
+ return log(`FAIL ${error}`), { passed, failed: [{ name: "authenticated", error }] };
1430
+ }
1431
+ if (status === 404) {
1432
+ let error = `Nothing at ${base}/resources. Check the base URL, usually https://your-app/api/lopebase.`;
1433
+ return log(`FAIL ${error}`), { passed, failed: [{ name: "found", error }] };
1434
+ }
1435
+ for (let c of conformanceChecks({
1436
+ baseUrl: input.baseUrl,
1437
+ secret: input.secret,
1438
+ token: input.token
1439
+ }))
1440
+ try {
1441
+ await c.run(), passed++, log(`pass ${c.name}`);
1442
+ } catch (e2) {
1443
+ let error = e2 instanceof Error ? e2.message.split(`
1444
+ `)[0] : String(e2);
1445
+ failed.push({ name: c.name, error }), log(`FAIL ${c.name}: ${error}`);
1446
+ }
1447
+ return { passed, failed };
1448
+ }
817
1449
 
818
1450
  // src/agents.ts
1451
+ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
819
1452
  import { spawnSync } from "node:child_process";
820
1453
  var AGENTS = [
821
1454
  {
822
1455
  id: "claude",
823
1456
  label: "Claude Code",
824
1457
  bin: "claude",
825
- args: (p) => [p],
1458
+ args: (p2) => [p2],
826
1459
  mcpAdd: (argv) => [
827
1460
  "mcp",
828
1461
  "add",
@@ -837,32 +1470,48 @@ var AGENTS = [
837
1470
  id: "codex",
838
1471
  label: "Codex",
839
1472
  bin: "codex",
840
- args: (p) => [p],
1473
+ args: (p2) => [p2],
841
1474
  mcpAdd: (argv) => ["mcp", "add", "lopebase", "--", ...argv]
842
1475
  },
843
1476
  {
844
1477
  id: "gemini",
845
1478
  label: "Gemini CLI",
846
1479
  bin: "gemini",
847
- args: (p) => ["-i", p],
1480
+ args: (p2) => ["-i", p2],
848
1481
  mcpAdd: (argv) => ["mcp", "add", "lopebase", ...argv]
849
1482
  },
850
1483
  {
851
1484
  id: "opencode",
852
1485
  label: "opencode",
853
1486
  bin: "opencode",
854
- args: (p) => ["--prompt", p]
1487
+ args: (p2) => ["--prompt", p2]
855
1488
  },
856
1489
  {
857
1490
  id: "cursor-agent",
858
1491
  label: "Cursor Agent",
859
1492
  bin: "cursor-agent",
860
- args: (p) => [p]
1493
+ args: (p2) => [p2]
861
1494
  }
862
1495
  ];
863
1496
  function isInstalled(bin) {
864
- let probe = process.platform === "win32" ? "where" : "which";
865
- return spawnSync(probe, [bin], { stdio: "ignore" }).status === 0;
1497
+ return whereIs(bin) !== null;
1498
+ }
1499
+ function whereIs(bin) {
1500
+ let probe = process.platform === "win32" ? "where" : "which", res = spawnSync(probe, [bin], { encoding: "utf8" });
1501
+ if (res.status !== 0)
1502
+ return null;
1503
+ return res.stdout.split(/\r?\n/)[0]?.trim() || null;
1504
+ }
1505
+ function portableArgs(bin, args, platform = process.platform, resolved = platform === "win32" ? whereIs(bin) : null) {
1506
+ if (platform !== "win32" || !resolved || !/\.(cmd|bat)$/i.test(resolved))
1507
+ return args;
1508
+ return args.map((a) => a.replace(/[ \t]*\r?\n\s*/g, " "));
1509
+ }
1510
+ function spawnAgent(bin, args, options) {
1511
+ return import_cross_spawn.default(bin, portableArgs(bin, args), options);
1512
+ }
1513
+ function spawnAgentSync(bin, args, options) {
1514
+ return import_cross_spawn.default.sync(bin, portableArgs(bin, args), options);
866
1515
  }
867
1516
  function installedAgents() {
868
1517
  return AGENTS.filter((a) => isInstalled(a.bin));
@@ -879,6 +1528,22 @@ import {
879
1528
  import os from "node:os";
880
1529
  import path from "node:path";
881
1530
  var DEFAULT_URL = "https://app.lopebase.com";
1531
+ function workspaceUrl(input) {
1532
+ let v2 = input.trim().replace(/\/+$/, "");
1533
+ if (!v2)
1534
+ return null;
1535
+ if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(v2))
1536
+ return `https://${v2.toLowerCase()}.lopebase.com`;
1537
+ let withScheme = /^https?:\/\//i.test(v2) ? v2 : `https://${v2}`;
1538
+ try {
1539
+ let u2 = new URL(withScheme);
1540
+ if (!u2.hostname.includes(".") && u2.hostname !== "localhost")
1541
+ return null;
1542
+ return `${u2.origin}${u2.pathname.replace(/\/+$/, "")}`;
1543
+ } catch {
1544
+ return null;
1545
+ }
1546
+ }
882
1547
  function configDir() {
883
1548
  let base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
884
1549
  return path.join(base, "lopebase");
@@ -908,21 +1573,21 @@ function saveAuth(auth) {
908
1573
  var mcpEndpoint = (url) => `${url.replace(/\/+$/, "")}/api/mcp`;
909
1574
 
910
1575
  // src/detect.ts
911
- var APP_SIDE_ENV = /^(NEXT_PUBLIC_|PUBLIC_|VITE_|NUXT_PUBLIC_|EXPO_PUBLIC_)|_DSN$/;
1576
+ var APP_SIDE_ENV = /^(NEXT_PUBLIC_|PUBLIC_|VITE_|NUXT_PUBLIC_|EXPO_PUBLIC_|REACT_APP_|GATSBY_)|_DSN$/, file = (name) => new RegExp(`(^|/)${name.replace(/[.]/g, "\\.")}$`);
912
1577
  function evidenceFor(facts, rule) {
913
1578
  let out = [], weak = [];
914
1579
  for (let re of rule.deps ?? []) {
915
- let hit = facts.dependencies.find((d) => re.test(d));
1580
+ let hit = facts.dependencies.find((d3) => re.test(d3));
916
1581
  if (hit)
917
- out.push(`uses ${hit}`);
1582
+ out.push(`uses ${hit.replace(/^(py|gem|composer|go|cargo):/, "")}`);
918
1583
  }
919
1584
  for (let re of rule.files ?? []) {
920
1585
  let hit = facts.files.find((f) => re.test(f));
921
1586
  if (hit)
922
- out.push(`has ${hit}`);
1587
+ out.push(`has ${hit.replace(/#.*$/, "")}`);
923
1588
  }
924
1589
  for (let re of rule.env ?? []) {
925
- let hit = facts.envKeys.find((k) => re.test(k));
1590
+ let hit = facts.envKeys.find((k3) => re.test(k3));
926
1591
  if (hit)
927
1592
  (APP_SIDE_ENV.test(hit) ? out : weak).push(`env ${hit}`);
928
1593
  }
@@ -931,15 +1596,31 @@ function evidenceFor(facts, rule) {
931
1596
  var SOURCE_RULES = {
932
1597
  posthog: {
933
1598
  label: "PostHog",
934
- rule: { deps: [/^posthog-(js|node)$/], env: [/POSTHOG/] },
1599
+ rule: {
1600
+ deps: [
1601
+ /^posthog-(js|node|react-native)$/,
1602
+ /^py:posthog$/,
1603
+ /^gem:posthog-ruby$/,
1604
+ /^composer:posthog\//,
1605
+ /^go:github\.com\/posthog\//
1606
+ ],
1607
+ env: [/POSTHOG/]
1608
+ },
935
1609
  why: "Signups, active users, and funnels on your dashboard."
936
1610
  },
937
1611
  ga4: {
938
1612
  label: "Google Analytics",
939
1613
  rule: {
940
- deps: [/^@next\/third-parties$/, /^react-ga4$/, /^vue-gtag/],
1614
+ deps: [
1615
+ /^@next\/third-parties$/,
1616
+ /^react-ga4$/,
1617
+ /^vue-gtag/,
1618
+ /^nuxt-gtag$/,
1619
+ /^gatsby-plugin-google-(gtag|analytics)$/,
1620
+ /^@analytics\/google-analytics$/
1621
+ ],
941
1622
  env: [
942
- /^(NEXT_PUBLIC_|PUBLIC_|VITE_)?GA(4)?_(MEASUREMENT_)?ID$/,
1623
+ /^(NEXT_PUBLIC_|PUBLIC_|VITE_|GATSBY_|REACT_APP_)?GA(4)?_(MEASUREMENT_|TRACKING_)?ID$/,
943
1624
  /GTM_ID/
944
1625
  ]
945
1626
  },
@@ -948,7 +1629,14 @@ var SOURCE_RULES = {
948
1629
  sentry: {
949
1630
  label: "Sentry",
950
1631
  rule: {
951
- deps: [/^@sentry\//],
1632
+ deps: [
1633
+ /^@sentry\//,
1634
+ /^py:sentry-sdk$/,
1635
+ /^gem:sentry-(ruby|rails)$/,
1636
+ /^composer:sentry\//,
1637
+ /^go:github\.com\/getsentry\//,
1638
+ /^cargo:sentry$/
1639
+ ],
952
1640
  files: [/sentry\.(client|server|edge)\.config/],
953
1641
  env: [/SENTRY_DSN/]
954
1642
  },
@@ -956,80 +1644,392 @@ var SOURCE_RULES = {
956
1644
  },
957
1645
  stripe: {
958
1646
  label: "Stripe",
959
- rule: { deps: [/^stripe$/, /^@stripe\//], env: [/^STRIPE_/] },
1647
+ rule: {
1648
+ deps: [
1649
+ /^stripe$/,
1650
+ /^@stripe\//,
1651
+ /^py:stripe$/,
1652
+ /^gem:stripe$/,
1653
+ /^composer:stripe\//,
1654
+ /^go:github\.com\/stripe\//
1655
+ ],
1656
+ env: [/^STRIPE_/, /^(NEXT_PUBLIC_|PUBLIC_|VITE_)STRIPE_/]
1657
+ },
960
1658
  why: "Revenue, new customers, and churn per product."
1659
+ },
1660
+ polar: {
1661
+ label: "Polar",
1662
+ rule: { deps: [/^@polar-sh\//, /^py:polar-sdk$/], env: [/^POLAR_/] },
1663
+ why: "Revenue and subscriptions from Polar per product."
1664
+ },
1665
+ supabase: {
1666
+ label: "Supabase",
1667
+ rule: {
1668
+ deps: [/^@supabase\//, /^py:supabase$/, /^composer:supabase\//],
1669
+ files: [file("supabase/config.toml")],
1670
+ env: [/SUPABASE_URL$/]
1671
+ },
1672
+ why: "Database health, security warnings, disk, and backups next to everything else."
1673
+ },
1674
+ resend: {
1675
+ label: "Resend",
1676
+ rule: {
1677
+ deps: [/^resend$/, /^py:resend$/, /^gem:resend$/, /^composer:resend\//],
1678
+ env: [/^RESEND_/]
1679
+ },
1680
+ why: "Bounce and complaint rates, so you know your emails land."
1681
+ },
1682
+ sequenzy: {
1683
+ label: "Sequenzy",
1684
+ rule: { deps: [/^@?sequenzy/], env: [/SEQUENZY/] },
1685
+ why: "Your audience and running email sequences at a glance."
961
1686
  }
962
1687
  }, STACK_RULES = [
963
- { kind: "framework", name: "Next.js", rule: { deps: [/^next$/] } },
964
- { kind: "framework", name: "Astro", rule: { deps: [/^astro$/] } },
1688
+ {
1689
+ kind: "framework",
1690
+ name: "Next.js",
1691
+ rule: { deps: [/^next$/], files: [/(^|\/)next\.config\./] }
1692
+ },
1693
+ {
1694
+ kind: "framework",
1695
+ name: "Astro",
1696
+ rule: { deps: [/^astro$/], files: [/(^|\/)astro\.config\./] }
1697
+ },
965
1698
  {
966
1699
  kind: "framework",
967
1700
  name: "SvelteKit",
968
1701
  rule: { deps: [/^@sveltejs\/kit$/] }
969
1702
  },
1703
+ { kind: "framework", name: "Remix", rule: { deps: [/^@remix-run\//] } },
1704
+ {
1705
+ kind: "framework",
1706
+ name: "React Router",
1707
+ rule: { deps: [/^@react-router\/dev$/] }
1708
+ },
1709
+ {
1710
+ kind: "framework",
1711
+ name: "Nuxt",
1712
+ rule: { deps: [/^nuxt$/], files: [/(^|\/)nuxt\.config\./] }
1713
+ },
1714
+ { kind: "framework", name: "Gatsby", rule: { deps: [/^gatsby$/] } },
1715
+ {
1716
+ kind: "framework",
1717
+ name: "Docusaurus",
1718
+ rule: { deps: [/^@docusaurus\/core$/] }
1719
+ },
1720
+ {
1721
+ kind: "framework",
1722
+ name: "Eleventy",
1723
+ rule: {
1724
+ deps: [/^@11ty\/eleventy$/],
1725
+ files: [/(^|\/)(\.eleventy\.js|eleventy\.config\.)/]
1726
+ }
1727
+ },
1728
+ {
1729
+ kind: "framework",
1730
+ name: "Hugo",
1731
+ rule: { files: [/(^|\/)hugo\.(toml|ya?ml|json)$/] }
1732
+ },
1733
+ { kind: "framework", name: "Jekyll", rule: { deps: [/^gem:jekyll$/] } },
1734
+ { kind: "framework", name: "Angular", rule: { deps: [/^@angular\/core$/] } },
1735
+ {
1736
+ kind: "framework",
1737
+ name: "SolidStart",
1738
+ rule: { deps: [/^@solidjs\/start$/] }
1739
+ },
970
1740
  {
971
1741
  kind: "framework",
972
- name: "Remix",
973
- rule: { deps: [/^@remix-run\//, /^react-router$/] }
1742
+ name: "Qwik",
1743
+ rule: { deps: [/^@builder\.io\/qwik-city$/] }
974
1744
  },
975
- { kind: "framework", name: "Nuxt", rule: { deps: [/^nuxt$/] } },
1745
+ {
1746
+ kind: "framework",
1747
+ name: "TanStack Start",
1748
+ rule: { deps: [/^@tanstack\/(react-)?start$/] }
1749
+ },
1750
+ { kind: "framework", name: "Expo", rule: { deps: [/^expo$/] } },
1751
+ { kind: "framework", name: "Electron", rule: { deps: [/^electron$/] } },
976
1752
  { kind: "framework", name: "Hono", rule: { deps: [/^hono$/] } },
1753
+ { kind: "framework", name: "Express", rule: { deps: [/^express$/] } },
1754
+ { kind: "framework", name: "Fastify", rule: { deps: [/^fastify$/] } },
1755
+ { kind: "framework", name: "NestJS", rule: { deps: [/^@nestjs\/core$/] } },
1756
+ {
1757
+ kind: "framework",
1758
+ name: "Django",
1759
+ rule: { deps: [/^py:django$/], files: [/(^|\/)manage\.py$/] }
1760
+ },
1761
+ { kind: "framework", name: "Flask", rule: { deps: [/^py:flask$/] } },
1762
+ { kind: "framework", name: "FastAPI", rule: { deps: [/^py:fastapi$/] } },
1763
+ { kind: "framework", name: "Rails", rule: { deps: [/^gem:rails$/] } },
1764
+ {
1765
+ kind: "framework",
1766
+ name: "Laravel",
1767
+ rule: {
1768
+ deps: [/^composer:laravel\/framework$/],
1769
+ files: [/(^|\/)artisan$/]
1770
+ }
1771
+ },
1772
+ {
1773
+ kind: "framework",
1774
+ name: "WordPress",
1775
+ rule: { files: [/(^|\/)wp-config(-sample)?\.php$/, /(^|\/)wp-content$/] }
1776
+ },
1777
+ {
1778
+ kind: "framework",
1779
+ name: "Shopify app",
1780
+ rule: { files: [/(^|\/)shopify\.app\.toml$/] }
1781
+ },
1782
+ { kind: "framework", name: "Vue", rule: { deps: [/^vue$/] } },
1783
+ { kind: "framework", name: "React", rule: { deps: [/^react$/] } },
1784
+ { kind: "framework", name: "Vite", rule: { deps: [/^vite$/] } },
1785
+ { kind: "language", name: "TypeScript", rule: { deps: [/^typescript$/] } },
1786
+ {
1787
+ kind: "language",
1788
+ name: "Python",
1789
+ rule: { files: [/(^|\/)(requirements\.txt|pyproject\.toml|Pipfile)$/] }
1790
+ },
1791
+ { kind: "language", name: "Ruby", rule: { files: [/(^|\/)Gemfile$/] } },
1792
+ { kind: "language", name: "PHP", rule: { files: [/(^|\/)composer\.json$/] } },
1793
+ { kind: "language", name: "Go", rule: { files: [/(^|\/)go\.mod$/] } },
1794
+ { kind: "language", name: "Rust", rule: { files: [/(^|\/)Cargo\.toml$/] } },
977
1795
  {
978
1796
  kind: "hosting",
979
1797
  name: "Cloudflare Workers",
980
1798
  rule: {
981
1799
  files: [/(^|\/)wrangler\.(toml|jsonc?)$/],
982
- deps: [/^@opennextjs\/cloudflare$/]
1800
+ deps: [
1801
+ /^@opennextjs\/cloudflare$/,
1802
+ /^@astrojs\/cloudflare$/,
1803
+ /^@sveltejs\/adapter-cloudflare/
1804
+ ]
1805
+ }
1806
+ },
1807
+ {
1808
+ kind: "hosting",
1809
+ name: "Vercel",
1810
+ rule: {
1811
+ files: [file("vercel.json")],
1812
+ deps: [
1813
+ /^@vercel\/(analytics|speed-insights|og)$/,
1814
+ /^@astrojs\/vercel$/,
1815
+ /^@sveltejs\/adapter-vercel$/
1816
+ ]
983
1817
  }
984
1818
  },
985
- { kind: "hosting", name: "Vercel", rule: { files: [/(^|\/)vercel\.json$/] } },
986
1819
  {
987
1820
  kind: "hosting",
988
1821
  name: "Netlify",
989
- rule: { files: [/(^|\/)netlify\.toml$/] }
1822
+ rule: {
1823
+ files: [file("netlify.toml")],
1824
+ deps: [/^@netlify\//, /^@astrojs\/netlify$/]
1825
+ }
990
1826
  },
991
- { kind: "hosting", name: "Fly.io", rule: { files: [/(^|\/)fly\.toml$/] } },
1827
+ { kind: "hosting", name: "Fly.io", rule: { files: [file("fly.toml")] } },
992
1828
  {
993
- kind: "database",
994
- name: "Supabase",
995
- rule: { deps: [/^@supabase\//], files: [/(^|\/)supabase\/config\.toml$/] }
1829
+ kind: "hosting",
1830
+ name: "Render",
1831
+ rule: { files: [/(^|\/)render\.ya?ml$/] }
996
1832
  },
1833
+ {
1834
+ kind: "hosting",
1835
+ name: "Railway",
1836
+ rule: { files: [/(^|\/)railway\.(json|toml)$/] }
1837
+ },
1838
+ {
1839
+ kind: "hosting",
1840
+ name: "Firebase Hosting",
1841
+ rule: { files: [file("firebase.json")] }
1842
+ },
1843
+ {
1844
+ kind: "hosting",
1845
+ name: "AWS Amplify",
1846
+ rule: { files: [/(^|\/)amplify\.ya?ml$/] }
1847
+ },
1848
+ { kind: "hosting", name: "Heroku", rule: { files: [file("Procfile")] } },
1849
+ { kind: "hosting", name: "GitHub Pages", rule: { files: [file("CNAME")] } },
1850
+ {
1851
+ kind: "hosting",
1852
+ name: "Docker",
1853
+ rule: { files: [file("Dockerfile"), /(^|\/)(docker-)?compose\.ya?ml$/] }
1854
+ },
1855
+ { kind: "database", name: "Supabase", rule: SOURCE_RULES.supabase.rule },
997
1856
  {
998
1857
  kind: "database",
999
1858
  name: "Convex",
1000
- rule: { deps: [/^convex$/], files: [/(^|\/)convex\/schema\.ts$/] }
1859
+ rule: { deps: [/^convex$/], files: [file("convex/schema.ts")] }
1001
1860
  },
1861
+ { kind: "database", name: "Neon", rule: { deps: [/^@neondatabase\//] } },
1002
1862
  {
1003
1863
  kind: "database",
1004
- name: "Neon",
1005
- rule: { deps: [/^@neondatabase\//, /^@neon\/(env|config)$/] }
1864
+ name: "Postgres",
1865
+ rule: {
1866
+ deps: [
1867
+ /^postgres$/,
1868
+ /^pg$/,
1869
+ /^py:psycopg2?(-binary)?$/,
1870
+ /^py:asyncpg$/,
1871
+ /^gem:pg$/
1872
+ ]
1873
+ }
1006
1874
  },
1007
1875
  {
1008
1876
  kind: "database",
1009
- name: "Postgres",
1010
- rule: { deps: [/^postgres$/, /^pg$/] }
1877
+ name: "MySQL",
1878
+ rule: { deps: [/^mysql2?$/, /^py:(mysqlclient|pymysql)$/, /^gem:mysql2$/] }
1879
+ },
1880
+ {
1881
+ kind: "database",
1882
+ name: "SQLite",
1883
+ rule: { deps: [/^better-sqlite3$/, /^gem:sqlite3$/] }
1884
+ },
1885
+ {
1886
+ kind: "database",
1887
+ name: "MongoDB",
1888
+ rule: { deps: [/^mongodb$/, /^mongoose$/, /^py:pymongo$/, /^py:motor$/] }
1889
+ },
1890
+ {
1891
+ kind: "database",
1892
+ name: "Firebase",
1893
+ rule: { deps: [/^firebase(-admin)?$/, /^py:firebase-admin$/] }
1894
+ },
1895
+ {
1896
+ kind: "database",
1897
+ name: "PlanetScale",
1898
+ rule: { deps: [/^@planetscale\/database$/] }
1899
+ },
1900
+ { kind: "database", name: "Turso", rule: { deps: [/^@libsql\/client$/] } },
1901
+ {
1902
+ kind: "database",
1903
+ name: "Redis",
1904
+ rule: { deps: [/^@upstash\/redis$/, /^ioredis$/, /^redis$/, /^py:redis$/] }
1905
+ },
1906
+ {
1907
+ kind: "database",
1908
+ name: "Prisma",
1909
+ rule: {
1910
+ deps: [/^@prisma\/client$/],
1911
+ files: [file("prisma/schema.prisma")]
1912
+ }
1011
1913
  },
1012
- { kind: "database", name: "Prisma", rule: { deps: [/^@prisma\/client$/] } },
1013
1914
  { kind: "database", name: "Drizzle", rule: { deps: [/^drizzle-orm$/] } },
1014
1915
  {
1015
1916
  kind: "database",
1016
1917
  name: "Cloudflare D1",
1017
1918
  rule: { files: [/(^|\/)wrangler\.(toml|jsonc?)#d1$/] }
1018
1919
  },
1920
+ { kind: "email", name: "Resend", rule: SOURCE_RULES.resend.rule },
1921
+ { kind: "email", name: "Sequenzy", rule: SOURCE_RULES.sequenzy.rule },
1922
+ {
1923
+ kind: "email",
1924
+ name: "SendGrid",
1925
+ rule: { deps: [/^@sendgrid\/mail$/, /^py:sendgrid$/], env: [/^SENDGRID_/] }
1926
+ },
1927
+ {
1928
+ kind: "email",
1929
+ name: "Postmark",
1930
+ rule: { deps: [/^postmark$/, /^py:postmarker$/], env: [/^POSTMARK_/] }
1931
+ },
1019
1932
  {
1020
1933
  kind: "email",
1021
- name: "Resend",
1022
- rule: { deps: [/^resend$/], env: [/^RESEND_/] }
1934
+ name: "Mailgun",
1935
+ rule: { deps: [/^mailgun\.js$/], env: [/^MAILGUN_/] }
1936
+ },
1937
+ {
1938
+ kind: "email",
1939
+ name: "Loops",
1940
+ rule: { deps: [/^loops$/], env: [/^LOOPS_/] }
1941
+ },
1942
+ {
1943
+ kind: "email",
1944
+ name: "Amazon SES",
1945
+ rule: { deps: [/^@aws-sdk\/client-sesv?2?$/] }
1946
+ },
1947
+ { kind: "email", name: "Nodemailer", rule: { deps: [/^nodemailer$/] } },
1948
+ { kind: "billing", name: "Stripe", rule: SOURCE_RULES.stripe.rule },
1949
+ { kind: "billing", name: "Polar", rule: SOURCE_RULES.polar.rule },
1950
+ {
1951
+ kind: "billing",
1952
+ name: "Lemon Squeezy",
1953
+ rule: { deps: [/^@lemonsqueezy\//], env: [/^LEMON_?SQUEEZY_/] }
1023
1954
  },
1024
1955
  {
1025
1956
  kind: "billing",
1026
- name: "Polar",
1027
- rule: { deps: [/^@polar-sh\//], env: [/^POLAR_/] }
1957
+ name: "Paddle",
1958
+ rule: { deps: [/^@paddle\//], env: [/^PADDLE_/] }
1028
1959
  },
1029
1960
  { kind: "auth", name: "Clerk", rule: { deps: [/^@clerk\//] } },
1030
- { kind: "auth", name: "WorkOS", rule: { deps: [/^@workos-inc\//] } },
1031
- { kind: "auth", name: "Better Auth", rule: { deps: [/^better-auth$/] } }
1032
- ];
1961
+ {
1962
+ kind: "auth",
1963
+ name: "WorkOS",
1964
+ rule: { deps: [/^@workos-inc\//, /^py:workos$/] }
1965
+ },
1966
+ { kind: "auth", name: "Better Auth", rule: { deps: [/^better-auth$/] } },
1967
+ {
1968
+ kind: "auth",
1969
+ name: "Auth.js",
1970
+ rule: { deps: [/^next-auth$/, /^@auth\/core$/] }
1971
+ },
1972
+ { kind: "auth", name: "Auth0", rule: { deps: [/^@auth0\//] } },
1973
+ { kind: "auth", name: "Kinde", rule: { deps: [/^@kinde-oss\//] } },
1974
+ { kind: "auth", name: "Lucia", rule: { deps: [/^lucia$/] } },
1975
+ { kind: "auth", name: "Devise", rule: { deps: [/^gem:devise$/] } },
1976
+ {
1977
+ kind: "analytics",
1978
+ name: "Vercel Analytics",
1979
+ rule: { deps: [/^@vercel\/analytics$/] }
1980
+ },
1981
+ {
1982
+ kind: "analytics",
1983
+ name: "Plausible",
1984
+ rule: { deps: [/plausible/], env: [/PLAUSIBLE/] }
1985
+ },
1986
+ { kind: "analytics", name: "Umami", rule: { env: [/UMAMI/] } },
1987
+ { kind: "analytics", name: "Fathom", rule: { deps: [/^fathom-client$/] } },
1988
+ {
1989
+ kind: "analytics",
1990
+ name: "Mixpanel",
1991
+ rule: { deps: [/^mixpanel(-browser)?$/] }
1992
+ },
1993
+ { kind: "analytics", name: "Amplitude", rule: { deps: [/^@amplitude\//] } },
1994
+ {
1995
+ kind: "cms",
1996
+ name: "Sanity",
1997
+ rule: { deps: [/^(next-)?sanity$/, /^@sanity\/client$/] }
1998
+ },
1999
+ { kind: "cms", name: "Contentful", rule: { deps: [/^contentful$/] } },
2000
+ { kind: "cms", name: "Payload", rule: { deps: [/^payload$/] } },
2001
+ { kind: "cms", name: "Strapi", rule: { deps: [/^@strapi\/strapi$/] } },
2002
+ { kind: "cms", name: "Keystatic", rule: { deps: [/^@keystatic\/core$/] } },
2003
+ {
2004
+ kind: "cms",
2005
+ name: "MDX",
2006
+ rule: { deps: [/^@next\/mdx$/, /^@astrojs\/mdx$/, /^@mdx-js\//] }
2007
+ }
2008
+ ], WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
2009
+ "Next.js",
2010
+ "Astro",
2011
+ "SvelteKit",
2012
+ "Remix",
2013
+ "React Router",
2014
+ "Nuxt",
2015
+ "Gatsby",
2016
+ "Docusaurus",
2017
+ "Eleventy",
2018
+ "Hugo",
2019
+ "Jekyll",
2020
+ "Angular",
2021
+ "SolidStart",
2022
+ "Qwik",
2023
+ "TanStack Start",
2024
+ "Django",
2025
+ "Flask",
2026
+ "Rails",
2027
+ "Laravel",
2028
+ "WordPress",
2029
+ "Vue",
2030
+ "React",
2031
+ "Vite"
2032
+ ]);
1033
2033
  function detectStack(facts) {
1034
2034
  let stack = [];
1035
2035
  for (let s of STACK_RULES) {
@@ -1037,7 +2037,13 @@ function detectStack(facts) {
1037
2037
  if (ev.length)
1038
2038
  stack.push({ kind: s.kind, name: s.name, evidence: ev[0] });
1039
2039
  }
1040
- let sources = [];
2040
+ if (!stack.some((s) => s.kind === "framework") && facts.files.some((f) => /(^|\/)index\.html$/.test(f)))
2041
+ stack.unshift({
2042
+ kind: "framework",
2043
+ name: "Static site",
2044
+ evidence: "has index.html"
2045
+ });
2046
+ let tidy = stack.filter((s) => s.kind === "framework" && !["React", "Vite", "Vue"].includes(s.name)).length ? stack.filter((s) => !["React", "Vite", "Vue"].includes(s.name)) : stack, sources = [];
1041
2047
  for (let [id, s] of Object.entries(SOURCE_RULES)) {
1042
2048
  let { strong, weak } = evidenceFor(facts, s.rule);
1043
2049
  if (strong.length || weak.length)
@@ -1049,7 +2055,15 @@ function detectStack(facts) {
1049
2055
  why: s.why
1050
2056
  });
1051
2057
  }
1052
- let has = (id) => sources.some((s) => s.source === id && s.status === "found"), listed = (id) => sources.some((s) => s.source === id), isWebsite = stack.some((s) => s.kind === "framework") || !!facts.siteUrl, gaps = [];
2058
+ if (facts.repo)
2059
+ sources.push({
2060
+ source: "github",
2061
+ label: "GitHub",
2062
+ status: "found",
2063
+ evidence: [`repo ${facts.repo}`],
2064
+ why: "Open pull requests, issues, and failing Actions runs."
2065
+ });
2066
+ let has = (id) => sources.some((s) => s.source === id && s.status === "found"), listed = (id) => sources.some((s) => s.source === id), isWebsite = tidy.some((s) => s.kind === "framework" && (WEB_FRAMEWORKS.has(s.name) || s.name === "Static site")) || !!facts.siteUrl, gaps = [];
1053
2067
  if (isWebsite)
1054
2068
  sources.push({
1055
2069
  source: "gsc",
@@ -1063,8 +2077,14 @@ function detectStack(facts) {
1063
2077
  status: "suggested",
1064
2078
  evidence: [],
1065
2079
  why: "Bing and ChatGPT search traffic. Imports from Search Console in one click."
2080
+ }, {
2081
+ source: "uptime",
2082
+ label: "Uptime check",
2083
+ status: "suggested",
2084
+ evidence: [],
2085
+ why: "Know the moment the site goes down. Nothing to install."
1066
2086
  });
1067
- if (!has("posthog") && !has("ga4")) {
2087
+ if (isWebsite && !has("posthog") && !has("ga4")) {
1068
2088
  if (gaps.push("No product analytics found. Add PostHog (free tier, one snippet) so LopeBase can show signups and active users."), !listed("posthog"))
1069
2089
  sources.push({
1070
2090
  source: "posthog",
@@ -1084,7 +2104,7 @@ function detectStack(facts) {
1084
2104
  why: SOURCE_RULES.sentry.why
1085
2105
  });
1086
2106
  }
1087
- return { stack, sources, gaps };
2107
+ return { stack: tidy, sources, gaps };
1088
2108
  }
1089
2109
 
1090
2110
  // src/device-login.ts
@@ -1101,27 +2121,27 @@ async function startDeviceLogin(url, opts = {}) {
1101
2121
  });
1102
2122
  if (!res.ok)
1103
2123
  return null;
1104
- let b = await res.json();
1105
- if (typeof b.device_code !== "string" || typeof b.user_code !== "string")
2124
+ let b3 = await res.json();
2125
+ if (typeof b3.device_code !== "string" || typeof b3.user_code !== "string")
1106
2126
  return null;
1107
2127
  return {
1108
- deviceCode: b.device_code,
1109
- userCode: b.user_code,
1110
- verifyUrl: String(b.verification_uri_complete ?? b.verification_uri),
1111
- interval: Number(b.interval) || 5,
1112
- expiresIn: Number(b.expires_in) || 600
2128
+ deviceCode: b3.device_code,
2129
+ userCode: b3.user_code,
2130
+ verifyUrl: String(b3.verification_uri_complete ?? b3.verification_uri),
2131
+ interval: Number(b3.interval) || 5,
2132
+ expiresIn: Number(b3.expires_in) || 600
1113
2133
  };
1114
2134
  } catch {
1115
2135
  return null;
1116
2136
  }
1117
2137
  }
1118
2138
  async function pollDeviceLogin(url, start, opts = {}) {
1119
- let f = opts.fetchImpl ?? fetch, sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))), interval = start.interval, deadline = Date.now() + start.expiresIn * 1000;
2139
+ let f = opts.fetchImpl ?? fetch, sleep = opts.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms))), interval = start.interval, deadline = Date.now() + start.expiresIn * 1000;
1120
2140
  while (Date.now() < deadline) {
1121
2141
  await sleep(interval * 1000);
1122
- let body;
2142
+ let body2;
1123
2143
  try {
1124
- body = await (await f(`${url}/api/oauth/token`, {
2144
+ body2 = await (await f(`${url}/api/oauth/token`, {
1125
2145
  method: "POST",
1126
2146
  headers: { "content-type": "application/x-www-form-urlencoded" },
1127
2147
  body: new URLSearchParams({
@@ -1132,14 +2152,14 @@ async function pollDeviceLogin(url, start, opts = {}) {
1132
2152
  } catch {
1133
2153
  continue;
1134
2154
  }
1135
- if (typeof body.access_token === "string")
2155
+ if (typeof body2.access_token === "string")
1136
2156
  return {
1137
2157
  ok: !0,
1138
- token: body.access_token,
1139
- url: typeof body.workspace_url === "string" ? body.workspace_url : url,
1140
- scope: String(body.scope ?? "")
2158
+ token: body2.access_token,
2159
+ url: typeof body2.workspace_url === "string" ? body2.workspace_url : url,
2160
+ scope: String(body2.scope ?? "")
1141
2161
  };
1142
- switch (body.error) {
2162
+ switch (body2.error) {
1143
2163
  case "authorization_pending":
1144
2164
  continue;
1145
2165
  case "slow_down":
@@ -1161,7 +2181,7 @@ async function pollDeviceLogin(url, start, opts = {}) {
1161
2181
  return {
1162
2182
  ok: !1,
1163
2183
  reason: "error",
1164
- message: String(body.error_description ?? body.error ?? "Sign-in failed.")
2184
+ message: String(body2.error_description ?? body2.error ?? "Sign-in failed.")
1165
2185
  };
1166
2186
  }
1167
2187
  }
@@ -1230,7 +2250,7 @@ async function runMcpProxy(auth, fetchImpl = fetch) {
1230
2250
  }
1231
2251
  process.stdout.write(`${text}
1232
2252
  `);
1233
- } catch (e) {
2253
+ } catch (e2) {
1234
2254
  if (id === null)
1235
2255
  return;
1236
2256
  write({
@@ -1238,7 +2258,7 @@ async function runMcpProxy(auth, fetchImpl = fetch) {
1238
2258
  id,
1239
2259
  error: {
1240
2260
  code: -32000,
1241
- message: `Could not reach LopeBase at ${endpoint}: ${String(e)}`
2261
+ message: `Could not reach LopeBase at ${endpoint}: ${String(e2)}`
1242
2262
  }
1243
2263
  });
1244
2264
  }
@@ -1270,116 +2290,499 @@ function onboardingPrompt(input) {
1270
2290
  }
1271
2291
 
1272
2292
  // src/scan.ts
1273
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
2293
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
1274
2294
  import path2 from "node:path";
1275
- var MONOREPO_DIRS = ["apps", "packages", "services", "web", "site"], MARKER_FILES = [
1276
- "wrangler.toml",
1277
- "wrangler.json",
1278
- "wrangler.jsonc",
1279
- "vercel.json",
1280
- "netlify.toml",
1281
- "fly.toml",
2295
+ var MONOREPO_DIRS = ["apps", "packages", "services", "sites", "web", "site"], SKIP_DIRS = /* @__PURE__ */ new Set([
2296
+ "node_modules",
2297
+ ".git",
2298
+ ".next",
2299
+ ".nuxt",
2300
+ ".svelte-kit",
2301
+ ".turbo",
2302
+ ".vercel",
2303
+ ".wrangler",
2304
+ "dist",
2305
+ "build",
2306
+ "out",
2307
+ "vendor",
2308
+ "venv",
2309
+ ".venv",
2310
+ "target"
2311
+ ]), MAX_PACKAGES = 200, MARKERS = [
2312
+ /^wrangler\.(toml|jsonc?)$/,
2313
+ /^vercel\.json$/,
2314
+ /^netlify\.toml$/,
2315
+ /^fly\.toml$/,
2316
+ /^render\.ya?ml$/,
2317
+ /^railway\.(json|toml)$/,
2318
+ /^firebase\.json$/,
2319
+ /^amplify\.ya?ml$/,
2320
+ /^app\.ya?ml$/,
2321
+ /^Dockerfile$/,
2322
+ /^docker-compose\.ya?ml$/,
2323
+ /^compose\.ya?ml$/,
2324
+ /^Procfile$/,
2325
+ /^CNAME$/,
2326
+ /^index\.html$/,
2327
+ /^(next|astro|nuxt|svelte|vite|remix|gatsby|docusaurus|tailwind|drizzle|sentry\.(client|server|edge))\.config\.(js|cjs|mjs|ts|mts)$/,
2328
+ /^gatsby-config\.(js|ts)$/,
2329
+ /^(\.eleventy\.js|eleventy\.config\.(js|cjs|mjs))$/,
2330
+ /^hugo\.(toml|ya?ml|json)$/,
2331
+ /^_config\.ya?ml$/,
2332
+ /^instrumentation(-client)?\.(ts|js)$/,
2333
+ /^manage\.py$/,
2334
+ /^artisan$/,
2335
+ /^wp-config(-sample)?\.php$/,
2336
+ /^shopify\.app\.toml$/,
2337
+ /^app\.json$/,
2338
+ /^(requirements\.txt|pyproject\.toml|Pipfile|Gemfile|composer\.json|go\.mod|Cargo\.toml)$/,
2339
+ /^(bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$/
2340
+ ], NESTED_MARKERS = [
1282
2341
  "supabase/config.toml",
1283
2342
  "convex/schema.ts",
1284
- "sentry.client.config.ts",
1285
- "sentry.server.config.ts",
1286
- "sentry.edge.config.ts",
1287
- "instrumentation.ts"
1288
- ], EXAMPLE_ENV = [
1289
- ".env.example",
1290
- ".env.sample",
1291
- ".env.template",
1292
- ".dev.vars.example"
1293
- ];
1294
- function readJson(file) {
2343
+ "prisma/schema.prisma",
2344
+ "src/instrumentation.ts",
2345
+ "wp-content",
2346
+ ".github/workflows",
2347
+ "content/config.ts"
2348
+ ], EXAMPLE_ENV = /^(\.env\.(example|sample|template|dist|local\.example|development\.example|production\.example)|example\.env|\.dev\.vars\.example)$/;
2349
+ function readText(file2) {
1295
2350
  try {
1296
- return JSON.parse(readFileSync2(file, "utf8"));
2351
+ return readFileSync2(file2, "utf8");
1297
2352
  } catch {
1298
2353
  return null;
1299
2354
  }
1300
2355
  }
2356
+ function readJson(file2) {
2357
+ let text = readText(file2);
2358
+ if (text === null)
2359
+ return null;
2360
+ try {
2361
+ return JSON.parse(text);
2362
+ } catch {
2363
+ return null;
2364
+ }
2365
+ }
2366
+ function isDir(p2) {
2367
+ try {
2368
+ return statSync(p2).isDirectory();
2369
+ } catch {
2370
+ return !1;
2371
+ }
2372
+ }
2373
+ function listDir(dir) {
2374
+ try {
2375
+ return readdirSync(dir);
2376
+ } catch {
2377
+ return [];
2378
+ }
2379
+ }
2380
+ function isPackage(dir) {
2381
+ return [
2382
+ "package.json",
2383
+ "pyproject.toml",
2384
+ "requirements.txt",
2385
+ "Gemfile",
2386
+ "composer.json",
2387
+ "go.mod",
2388
+ "Cargo.toml"
2389
+ ].some((f) => existsSync2(path2.join(dir, f)));
2390
+ }
2391
+ function workspaceGlobs(root) {
2392
+ let globs = [], ws = readJson(path2.join(root, "package.json"))?.workspaces, list = Array.isArray(ws) ? ws : ws && typeof ws === "object" && Array.isArray(ws.packages) ? ws.packages : [];
2393
+ for (let g2 of list)
2394
+ if (typeof g2 === "string")
2395
+ globs.push(g2);
2396
+ let pnpm = readText(path2.join(root, "pnpm-workspace.yaml"));
2397
+ if (pnpm)
2398
+ for (let m2 of pnpm.matchAll(/^\s*-\s*["']?([^"'\s#]+)["']?/gm))
2399
+ globs.push(m2[1]);
2400
+ return globs.filter((g2) => !g2.startsWith("!"));
2401
+ }
2402
+ function expandGlob(root, glob) {
2403
+ let clean = glob.replace(/\/\*\*$/, "/*").replace(/\/$/, "");
2404
+ if (!clean.includes("*")) {
2405
+ let dir = path2.join(root, clean);
2406
+ return isDir(dir) ? [dir] : [];
2407
+ }
2408
+ let [base] = clean.split("*"), parent = path2.join(root, base);
2409
+ return listDir(parent).filter((e2) => !SKIP_DIRS.has(e2) && !e2.startsWith(".")).map((e2) => path2.join(parent, e2)).filter(isDir);
2410
+ }
1301
2411
  function packageDirs(root) {
1302
- let dirs = [root];
2412
+ let dirs = /* @__PURE__ */ new Set([root]);
2413
+ for (let g2 of workspaceGlobs(root))
2414
+ for (let d3 of expandGlob(root, g2))
2415
+ if (isPackage(d3))
2416
+ dirs.add(d3);
1303
2417
  for (let group of MONOREPO_DIRS) {
1304
- let g = path2.join(root, group);
1305
- if (!existsSync2(g))
2418
+ let g2 = path2.join(root, group);
2419
+ if (!isDir(g2))
1306
2420
  continue;
1307
- if (existsSync2(path2.join(g, "package.json")))
1308
- dirs.push(g);
1309
- try {
1310
- for (let entry of readdirSync(g, { withFileTypes: !0 }))
1311
- if (entry.isDirectory() && existsSync2(path2.join(g, entry.name, "package.json")))
1312
- dirs.push(path2.join(g, entry.name));
1313
- } catch {}
2421
+ if (isPackage(g2))
2422
+ dirs.add(g2);
2423
+ for (let entry of listDir(g2)) {
2424
+ let d3 = path2.join(g2, entry);
2425
+ if (!SKIP_DIRS.has(entry) && isDir(d3) && isPackage(d3))
2426
+ dirs.add(d3);
2427
+ }
1314
2428
  }
1315
- return dirs;
2429
+ return [...dirs].slice(0, MAX_PACKAGES);
2430
+ }
2431
+ function pythonRequirements(text) {
2432
+ let out = [];
2433
+ for (let line of text.split(/\r?\n/)) {
2434
+ let m2 = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line.replace(/#.*/, ""));
2435
+ if (m2 && !line.trim().startsWith("-"))
2436
+ out.push(m2[1].toLowerCase());
2437
+ }
2438
+ return out;
2439
+ }
2440
+ function pythonProjectDeps(text) {
2441
+ let out = /* @__PURE__ */ new Set;
2442
+ for (let block of text.matchAll(/(?:^|\n)\s*dependencies\s*=\s*\[([\s\S]*?)\]/g))
2443
+ for (let m2 of block[1].matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g))
2444
+ out.add(m2[1].toLowerCase());
2445
+ for (let table of text.matchAll(/\[(?:tool\.poetry\.(?:group\.[a-z]+\.)?dependencies|(?:dev-)?packages)\]([\s\S]*?)(?=\n\[|$)/g))
2446
+ for (let m2 of table[1].matchAll(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/gm))
2447
+ out.add(m2[1].toLowerCase());
2448
+ return out.delete("python"), [...out];
2449
+ }
2450
+ function gemfileDeps(text) {
2451
+ return [...text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)].map((m2) => m2[1]);
2452
+ }
2453
+ function goModDeps(text) {
2454
+ let out = [];
2455
+ for (let m2 of text.matchAll(/^\s*(?:require\s+)?([a-z0-9.-]+\.[a-z]+\/[^\s]+)\s+v[0-9]/gm))
2456
+ out.push(m2[1]);
2457
+ return out;
2458
+ }
2459
+ function cargoDeps(text) {
2460
+ let out = [];
2461
+ for (let table of text.matchAll(/\[(?:dev-|build-)?dependencies\]([\s\S]*?)(?=\n\[|$)/g))
2462
+ for (let m2 of table[1].matchAll(/^\s*([A-Za-z0-9_-]+)\s*=/gm))
2463
+ out.push(m2[1]);
2464
+ return out;
2465
+ }
2466
+ function manifestDeps(dir) {
2467
+ let deps = [], add = (prefix, names) => {
2468
+ for (let n of names)
2469
+ deps.push(`${prefix}:${n}`);
2470
+ }, req = readText(path2.join(dir, "requirements.txt"));
2471
+ if (req)
2472
+ add("py", pythonRequirements(req));
2473
+ for (let f of ["pyproject.toml", "Pipfile"]) {
2474
+ let t = readText(path2.join(dir, f));
2475
+ if (t)
2476
+ add("py", pythonProjectDeps(t));
2477
+ }
2478
+ let gem = readText(path2.join(dir, "Gemfile"));
2479
+ if (gem)
2480
+ add("gem", gemfileDeps(gem));
2481
+ let composer = readJson(path2.join(dir, "composer.json"));
2482
+ for (let k3 of ["require", "require-dev"]) {
2483
+ let d3 = composer?.[k3];
2484
+ if (d3 && typeof d3 === "object")
2485
+ add("composer", Object.keys(d3));
2486
+ }
2487
+ let gomod = readText(path2.join(dir, "go.mod"));
2488
+ if (gomod)
2489
+ add("go", goModDeps(gomod));
2490
+ let cargo = readText(path2.join(dir, "Cargo.toml"));
2491
+ if (cargo)
2492
+ add("cargo", cargoDeps(cargo));
2493
+ return deps;
1316
2494
  }
1317
2495
  function siteFromWrangler(text) {
1318
2496
  let first = [
1319
2497
  ...text.matchAll(/(?:pattern|route)["']?\s*[:=]\s*["']([^"'*/\s]+)/g),
1320
2498
  ...text.matchAll(/custom_domain["']?\s*[:=]\s*["']([^"'*/\s]+)/g)
1321
- ].map((m) => m[1].toLowerCase()).filter((h) => h.includes(".") && !h.endsWith("workers.dev"))[0];
2499
+ ].map((m2) => m2[1].toLowerCase()).filter((h2) => h2.includes(".") && !h2.endsWith("workers.dev"))[0];
1322
2500
  if (!first)
1323
2501
  return;
1324
2502
  return `https://${first.replace(/^(app|www|api|console)\./, "")}`;
1325
2503
  }
2504
+ var NOT_A_SITE = /(^|\.)(localhost|example\.(com|org)|your-?(site|domain)\.[a-z]+|vercel\.app|netlify\.app|pages\.dev|workers\.dev|github\.com|gitlab\.com|npmjs\.com)$/;
2505
+ function publicSite(url) {
2506
+ try {
2507
+ let u2 = new URL(url.startsWith("http") ? url : `https://${url}`);
2508
+ if (!u2.hostname.includes(".") || NOT_A_SITE.test(u2.hostname))
2509
+ return;
2510
+ return `https://${u2.hostname.replace(/^www\./, "")}`;
2511
+ } catch {
2512
+ return;
2513
+ }
2514
+ }
2515
+ function siteFromConfig(text) {
2516
+ let m2 = /\b(?:site|siteUrl|baseURL|baseUrl|url)\s*[:=]\s*["'`](https?:\/\/[^"'`\s]+)["'`]/.exec(text);
2517
+ return m2 ? publicSite(m2[1]) : void 0;
2518
+ }
2519
+ function githubRepo(gitConfig) {
2520
+ let remotes = [
2521
+ ...gitConfig.matchAll(/\[remote "([^"]+)"\][^[]*?url\s*=\s*(\S+)/g)
2522
+ ].sort((a, b3) => Number(b3[1] === "origin") - Number(a[1] === "origin"));
2523
+ for (let [, , url] of remotes) {
2524
+ let m2 = /github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(url);
2525
+ if (m2)
2526
+ return `${m2[1]}/${m2[2]}`;
2527
+ }
2528
+ return;
2529
+ }
2530
+ function readGitConfig(root) {
2531
+ let dotGit = path2.join(root, ".git");
2532
+ if (isDir(dotGit))
2533
+ return readText(path2.join(dotGit, "config"));
2534
+ let pointer = /gitdir:\s*(.+)/.exec(readText(dotGit) ?? "")?.[1]?.trim();
2535
+ if (!pointer)
2536
+ return null;
2537
+ let gitDir = path2.resolve(root, pointer);
2538
+ return readText(path2.join(gitDir, "config")) ?? readText(path2.join(gitDir, "..", "..", "config"));
2539
+ }
2540
+ function siteFromHtml(html) {
2541
+ let m2 = /<link[^>]+rel=["']canonical["'][^>]+href=["'](https?:\/\/[^"']+)/i.exec(html) ?? /<link[^>]+href=["'](https?:\/\/[^"']+)["'][^>]+rel=["']canonical["']/i.exec(html) ?? /<meta[^>]+property=["']og:url["'][^>]+content=["'](https?:\/\/[^"']+)/i.exec(html);
2542
+ return m2 ? publicSite(m2[1]) : void 0;
2543
+ }
1326
2544
  function envKeyNames(text) {
1327
2545
  let out = [];
1328
2546
  for (let line of text.split(/\r?\n/)) {
1329
- let m = /^\s*(?:#\s*)?(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/.exec(line);
1330
- if (m)
1331
- out.push(m[1]);
2547
+ let m2 = /^\s*(?:#\s*)?(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/.exec(line);
2548
+ if (m2)
2549
+ out.push(m2[1]);
1332
2550
  }
1333
2551
  return out;
1334
2552
  }
1335
- function scanProject(root) {
1336
- let deps = /* @__PURE__ */ new Set, files = /* @__PURE__ */ new Set, envKeys = /* @__PURE__ */ new Set, name, siteUrl;
1337
- for (let dir of packageDirs(root)) {
1338
- let rel = (f) => path2.relative(root, path2.join(dir, f)) || f, pkg = readJson(path2.join(dir, "package.json"));
2553
+ var SITE_CONFIGS = /^(astro\.config\.\w+|next-sitemap\.config\.\w+|gatsby-config\.\w+|docusaurus\.config\.\w+|hugo\.toml|config\.toml|_config\.ya?ml|nuxt\.config\.\w+)$/;
2554
+ function scanProject(root, onProgress) {
2555
+ let deps = /* @__PURE__ */ new Set, files = /* @__PURE__ */ new Set, envKeys = /* @__PURE__ */ new Set, name, siteUrl, dirs = packageDirs(root), relDir = (d3) => path2.relative(root, d3).split(path2.sep).join("/") || ".";
2556
+ onProgress?.({
2557
+ label: `Found ${dirs.length} ${dirs.length === 1 ? "package" : "packages"}`,
2558
+ detail: dirs.length > 1 ? dirs.map(relDir).join(", ") : void 0
2559
+ });
2560
+ let envFiles = 0;
2561
+ for (let dir of dirs) {
2562
+ let rel = (f) => (path2.relative(root, path2.join(dir, f)) || f).split(path2.sep).join("/"), pkg = readJson(path2.join(dir, "package.json"));
1339
2563
  if (pkg) {
1340
2564
  if (dir === root && typeof pkg.name === "string")
1341
2565
  name = pkg.name;
1342
2566
  if (!siteUrl && typeof pkg.homepage === "string")
1343
- siteUrl = pkg.homepage;
1344
- for (let k of ["dependencies", "devDependencies"]) {
1345
- let d = pkg[k];
1346
- if (d && typeof d === "object")
1347
- for (let n of Object.keys(d))
2567
+ siteUrl = publicSite(pkg.homepage);
2568
+ for (let k3 of ["dependencies", "devDependencies"]) {
2569
+ let d3 = pkg[k3];
2570
+ if (d3 && typeof d3 === "object")
2571
+ for (let n of Object.keys(d3))
1348
2572
  deps.add(n);
1349
2573
  }
1350
2574
  }
1351
- for (let f of MARKER_FILES) {
1352
- if (!existsSync2(path2.join(dir, f)))
2575
+ for (let d3 of manifestDeps(dir))
2576
+ deps.add(d3);
2577
+ for (let entry of listDir(dir)) {
2578
+ if (EXAMPLE_ENV.test(entry)) {
2579
+ let text = readText(path2.join(dir, entry));
2580
+ if (text) {
2581
+ envFiles++;
2582
+ for (let k3 of envKeyNames(text))
2583
+ envKeys.add(k3);
2584
+ }
2585
+ }
2586
+ if (!MARKERS.some((re) => re.test(entry)))
1353
2587
  continue;
1354
- if (files.add(rel(f)), f.startsWith("wrangler."))
1355
- try {
1356
- let text = readFileSync2(path2.join(dir, f), "utf8");
1357
- if (/d1_databases/.test(text))
1358
- files.add(`${rel(f)}#d1`);
1359
- if (!siteUrl)
1360
- siteUrl = siteFromWrangler(text);
1361
- } catch {}
2588
+ if (files.add(rel(entry)), entry.startsWith("wrangler.")) {
2589
+ let text = readText(path2.join(dir, entry)) ?? "";
2590
+ if (/d1_databases/.test(text))
2591
+ files.add(`${rel(entry)}#d1`);
2592
+ if (!siteUrl)
2593
+ siteUrl = siteFromWrangler(text);
2594
+ } else if (entry === "CNAME" && !siteUrl)
2595
+ siteUrl = publicSite((readText(path2.join(dir, entry)) ?? "").trim());
2596
+ }
2597
+ for (let f of NESTED_MARKERS)
2598
+ if (existsSync2(path2.join(dir, f)))
2599
+ files.add(rel(f));
2600
+ if (!siteUrl)
2601
+ for (let entry of listDir(dir)) {
2602
+ if (!SITE_CONFIGS.test(entry))
2603
+ continue;
2604
+ if (siteUrl = siteFromConfig(readText(path2.join(dir, entry)) ?? ""), siteUrl)
2605
+ break;
2606
+ }
2607
+ if (!siteUrl) {
2608
+ for (let f of ["index.html", "public/index.html", "src/index.html"])
2609
+ if (siteUrl = siteFromHtml(readText(path2.join(dir, f)) ?? ""), siteUrl)
2610
+ break;
1362
2611
  }
1363
- for (let f of EXAMPLE_ENV)
1364
- try {
1365
- for (let k of envKeyNames(readFileSync2(path2.join(dir, f), "utf8")))
1366
- envKeys.add(k);
1367
- } catch {}
1368
2612
  }
2613
+ onProgress?.({
2614
+ label: `Read ${deps.size} ${deps.size === 1 ? "dependency" : "dependencies"}`
2615
+ }), onProgress?.({
2616
+ label: `Found ${files.size} config ${files.size === 1 ? "file" : "files"}`,
2617
+ detail: [...files].filter((f) => !f.includes("#")).slice(0, 8).join(", ") || void 0
2618
+ }), onProgress?.({
2619
+ label: envFiles ? `Read ${envKeys.size} variable names from ${envFiles} example env ${envFiles === 1 ? "file" : "files"}` : "No example env files to read",
2620
+ detail: "Names only. Your real .env files are never opened."
2621
+ });
2622
+ let gitConfig = readGitConfig(root), repo = gitConfig ? githubRepo(gitConfig) : void 0;
2623
+ if (repo)
2624
+ onProgress?.({ label: `GitHub repo ${repo}` });
1369
2625
  return {
1370
2626
  name,
1371
2627
  siteUrl,
2628
+ repo,
1372
2629
  dependencies: [...deps].sort(),
1373
2630
  files: [...files].sort(),
1374
2631
  envKeys: [...envKeys].sort()
1375
2632
  };
1376
2633
  }
1377
2634
 
2635
+ // src/ui.ts
2636
+ var import_picocolors3 = __toESM(require_picocolors(), 1), MARK = [
2637
+ " ▗▖ ▗▖ ",
2638
+ " ▐▌ ▐▌ ",
2639
+ " ▐█▖▗█▌ ",
2640
+ " ▜██▛ ",
2641
+ " ▟██▙ ",
2642
+ " ▟████▙ ",
2643
+ " ▟██████▙ ",
2644
+ " ",
2645
+ " ▀▀▀▀▀▀▀▀▀▀ ",
2646
+ " ▀▀▀▀▀▀ "
2647
+ ], HORIZON = /* @__PURE__ */ new Set([8, 9]);
2648
+ function motionAllowed(argv = process.argv, env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
2649
+ return isTTY && !env.CI && !env.LOPEBASE_NO_MOTION && env.TERM !== "dumb" && !argv.includes("--no-motion");
2650
+ }
2651
+ var sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
2652
+ function logoLines(opts) {
2653
+ let right = {
2654
+ 4: `${import_picocolors3.default.bold("Lope")}${import_picocolors3.default.dim("Base")}${opts.version ? import_picocolors3.default.dim(` v${opts.version}`) : ""}`,
2655
+ 5: import_picocolors3.default.dim(opts.tagline)
2656
+ };
2657
+ return MARK.map((row, i) => {
2658
+ let mark = HORIZON.has(i) ? import_picocolors3.default.yellow(import_picocolors3.default.dim(row)) : import_picocolors3.default.yellow(row);
2659
+ return right[i] ? `${mark} ${right[i]}` : mark;
2660
+ });
2661
+ }
2662
+ async function showLogo(opts) {
2663
+ let write = opts.write ?? ((s) => process.stdout.write(s)), lines = logoLines(opts);
2664
+ if (!opts.motion) {
2665
+ write(`
2666
+ ${lines.join(`
2667
+ `)}
2668
+
2669
+ `);
2670
+ return;
2671
+ }
2672
+ let blank = MARK.map((r2) => " ".repeat(r2.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 = () => {
2673
+ write(`\x1B[?25h
2674
+ `), process.exit(130);
2675
+ };
2676
+ process.once("SIGINT", onInterrupt), write(`
2677
+ \x1B[?25l`);
2678
+ try {
2679
+ let shown = /* @__PURE__ */ new Set;
2680
+ write(`${frame(shown).join(`
2681
+ `)}
2682
+ `);
2683
+ for (let i = MARK.length - 1;i >= 0; i--)
2684
+ shown.add(i), write(`${up}\r${frame(shown).join(`
2685
+ `)}
2686
+ `), await sleep(i >= 8 ? 70 : 35);
2687
+ write(`${up}\r${lines.join(`
2688
+ `)}
2689
+
2690
+ `);
2691
+ } finally {
2692
+ process.off("SIGINT", onInterrupt), write("\x1B[?25h");
2693
+ }
2694
+ }
2695
+ async function pace(motion, ms = 90) {
2696
+ if (motion)
2697
+ await sleep(ms);
2698
+ }
2699
+ var KIND_LABEL = {
2700
+ framework: "Framework",
2701
+ language: "Language",
2702
+ hosting: "Hosting",
2703
+ database: "Data",
2704
+ email: "Email",
2705
+ billing: "Billing",
2706
+ auth: "Sign-in",
2707
+ analytics: "Analytics",
2708
+ cms: "Content"
2709
+ };
2710
+ function stackRows(detection, facts) {
2711
+ let byKind = /* @__PURE__ */ new Map;
2712
+ for (let s of detection.stack)
2713
+ byKind.set(s.kind, [...byKind.get(s.kind) ?? [], s.name]);
2714
+ let rows = [];
2715
+ for (let kind of Object.keys(KIND_LABEL)) {
2716
+ let names = byKind.get(kind);
2717
+ if (names)
2718
+ rows.push([KIND_LABEL[kind], names.join(", ")]);
2719
+ }
2720
+ if (facts.siteUrl)
2721
+ rows.push(["Site", facts.siteUrl]);
2722
+ if (facts.repo)
2723
+ rows.push(["Repo", facts.repo]);
2724
+ if (!rows.length)
2725
+ return [import_picocolors3.default.dim("Nothing recognized yet. Your agent will look closer.")];
2726
+ let width = Math.max(...rows.map(([k3]) => k3.length));
2727
+ return rows.map(([k3, v2]) => `${import_picocolors3.default.dim(k3.padEnd(width))} ${v2}`);
2728
+ }
2729
+ var PLAN = [
2730
+ {
2731
+ title: "Scan this folder",
2732
+ detail: "Reads dependency and file names only. Never your .env or code."
2733
+ },
2734
+ {
2735
+ title: "Connect this computer",
2736
+ detail: "Sign in in the browser and confirm a short code."
2737
+ },
2738
+ {
2739
+ title: "Choose what to connect",
2740
+ detail: "What your code already uses is ticked. New things are optional."
2741
+ },
2742
+ {
2743
+ title: "Hand over to your coding agent",
2744
+ detail: "It does the setup and asks you when it needs a key or a yes."
2745
+ }
2746
+ ];
2747
+ function planLines() {
2748
+ return PLAN.map((s, i) => `${import_picocolors3.default.yellow(String(i + 1))} ${import_picocolors3.default.bold(s.title)}
2749
+ ${import_picocolors3.default.dim(s.detail)}`);
2750
+ }
2751
+ function stepTitle(n) {
2752
+ return `${import_picocolors3.default.dim(`Step ${n} of ${PLAN.length}`)} ${import_picocolors3.default.bold(PLAN[n - 1].title)}`;
2753
+ }
2754
+
1378
2755
  // bin/lopebase.ts
1379
- var [command = "init", ...rest] = process.argv.slice(2), flag = (name) => rest.includes(`--${name}`), option = (name) => {
2756
+ var [command = "init", ...rest] = process.argv.slice(2), HELP = `
2757
+ ${import_picocolors4.default.bgYellow(import_picocolors4.default.black(" LopeBase "))} Set up LopeBase from this project's folder.
2758
+
2759
+ ${import_picocolors4.default.bold("Usage")}
2760
+ npx lopebase scan, connect, and hand over to your agent
2761
+ npx lopebase login connect this computer (--paste for a token)
2762
+ npx lopebase scan [--json] show what the scan found
2763
+ npx lopebase prompt print the setup prompt instead
2764
+ npx lopebase mcp run the LopeBase tools as a stdio MCP server
2765
+ npx lopebase adapter secret print a new @lopebase/adapter signing secret
2766
+ npx lopebase adapter check <url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)
2767
+
2768
+ ${import_picocolors4.default.bold("Options")}
2769
+ --print print the agent prompt instead of starting an agent
2770
+ --no-motion no animation (also LOPEBASE_NO_MOTION=1)
2771
+
2772
+ ${import_picocolors4.default.bold("Learn more")}
2773
+ ${import_picocolors4.default.cyan("https://lopebase.com")}
2774
+ Help: contact@jackalope.digital
2775
+ `, flag = (name) => rest.includes(`--${name}`), option = (name) => {
1380
2776
  let i = rest.indexOf(`--${name}`);
1381
2777
  return i >= 0 ? rest[i + 1] : rest.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
1382
- }, cwd = process.cwd();
2778
+ }, cwd = process.cwd(), motion = motionAllowed(), TAGLINE = "Every product, one place.";
2779
+ function version() {
2780
+ try {
2781
+ return JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
2782
+ } catch {
2783
+ return;
2784
+ }
2785
+ }
1383
2786
  function bail(message) {
1384
2787
  xe(message), process.exit(1);
1385
2788
  }
@@ -1389,8 +2792,11 @@ function guard(value) {
1389
2792
  return value;
1390
2793
  }
1391
2794
  function openBrowser(url) {
1392
- let cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1393
- spawn(cmd, [url], { stdio: "ignore", detached: !0 }).unref();
2795
+ let [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
2796
+ try {
2797
+ let child = spawn2(cmd, args, { stdio: "ignore", detached: !0 });
2798
+ child.on("error", () => {}), child.unref();
2799
+ } catch {}
1394
2800
  }
1395
2801
  async function checkToken(auth) {
1396
2802
  try {
@@ -1409,40 +2815,40 @@ async function checkToken(auth) {
1409
2815
  });
1410
2816
  if (res.status === 401)
1411
2817
  return { ok: !1, message: "That token is not valid or has expired." };
1412
- let body = await res.json();
1413
- return { ok: !0, products: JSON.parse(body.result?.content?.[0]?.text ?? "[]").length };
1414
- } catch (e) {
2818
+ let body2 = await res.json();
2819
+ return { ok: !0, products: JSON.parse(body2.result?.content?.[0]?.text ?? "[]").length };
2820
+ } catch (e2) {
1415
2821
  return {
1416
2822
  ok: !1,
1417
- message: `Could not reach ${auth.url} (${String(e)}).`
2823
+ message: `Could not reach ${auth.url} (${String(e2)}).`
1418
2824
  };
1419
2825
  }
1420
2826
  }
1421
2827
  async function login() {
1422
- let url = guard(await he({
1423
- message: "Your LopeBase workspace address",
1424
- placeholder: "https://yourteam.lopebase.com",
1425
- initialValue: option("url") ?? loadAuth()?.url ?? DEFAULT_URL,
1426
- validate: (v) => /^https?:\/\/\S+$/.test(v ?? "") ? void 0 : "Starts with https://"
1427
- })).replace(/\/+$/, "");
2828
+ let known = option("url") ?? loadAuth()?.url, url = workspaceUrl(guard(await he({
2829
+ message: "Your LopeBase workspace (name or address)",
2830
+ placeholder: "yourteam or https://yourteam.lopebase.com",
2831
+ initialValue: known ?? "",
2832
+ validate: (v2) => workspaceUrl(v2 ?? "") ? void 0 : "Type your workspace name, like yourteam, or its full address"
2833
+ })));
1428
2834
  if (!flag("paste")) {
1429
2835
  let device = await startDeviceLogin(url);
1430
2836
  if (device) {
1431
2837
  Me([
1432
- `Your code: ${import_picocolors3.default.bold(import_picocolors3.default.yellow(device.userCode))}`,
2838
+ `Your code: ${import_picocolors4.default.bold(import_picocolors4.default.yellow(device.userCode))}`,
1433
2839
  "",
1434
2840
  "We opened your workspace in the browser. Sign in if asked,",
1435
2841
  "check the code matches, and press Connect."
1436
2842
  ].join(`
1437
- `), "Connect this computer"), openBrowser(device.verifyUrl), M2.info(import_picocolors3.default.dim(device.verifyUrl));
2843
+ `), "Connect this computer"), openBrowser(device.verifyUrl), M2.info(import_picocolors4.default.dim(device.verifyUrl));
1438
2844
  let s = Y2();
1439
2845
  s.start("Waiting for you to approve in the browser");
1440
2846
  let res = await pollDeviceLogin(url, device);
1441
2847
  if (res.ok) {
1442
- let file = saveAuth({ url: res.url, token: res.token });
1443
- return s.stop(`Connected (${res.scope}). Saved to ${import_picocolors3.default.dim(file)}`), { url: res.url, token: res.token };
2848
+ let file2 = saveAuth({ url: res.url, token: res.token });
2849
+ return s.stop(`Connected (${res.scope}). Saved to ${import_picocolors4.default.dim(file2)}`), { url: res.url, token: res.token };
1444
2850
  }
1445
- if (s.stop(import_picocolors3.default.red(res.message)), res.reason === "denied")
2851
+ if (s.stop(import_picocolors4.default.red(res.message)), res.reason === "denied")
1446
2852
  bail("Nothing was connected.");
1447
2853
  M2.warn("Falling back to pasting a token.");
1448
2854
  }
@@ -1453,47 +2859,59 @@ async function pasteLogin(url) {
1453
2859
  let settings = `${url}/settings#agent-access`;
1454
2860
  Me([
1455
2861
  "1. Sign in (or sign up) in the browser tab we open.",
1456
- `2. Under Settings, Agent access, press ${import_picocolors3.default.bold("Create agent access")}.`,
2862
+ `2. Under Settings, Agent access, press ${import_picocolors4.default.bold("Create agent access")}.`,
1457
2863
  "3. Copy the token and paste it here. It is saved on this computer only."
1458
2864
  ].join(`
1459
- `), "Get an agent token"), openBrowser(settings), M2.info(import_picocolors3.default.dim(settings));
2865
+ `), "Get an agent token"), openBrowser(settings), M2.info(import_picocolors4.default.dim(settings));
1460
2866
  for (;; ) {
1461
2867
  let token = guard(await ge({
1462
2868
  message: "Paste the agent token",
1463
- validate: (v) => v?.startsWith("lb_agent_") ? void 0 : "Tokens start with lb_agent_"
2869
+ validate: (v2) => v2?.startsWith("lb_agent_") ? void 0 : "Tokens start with lb_agent_"
1464
2870
  })).trim(), s = Y2();
1465
2871
  s.start("Checking the token");
1466
2872
  let check = await checkToken({ url, token });
1467
2873
  if (check.ok) {
1468
- let file = saveAuth({ url, token });
1469
- return s.stop(`Signed in. ${check.products} product(s) in this workspace. Saved to ${import_picocolors3.default.dim(file)}`), { url, token };
2874
+ let file2 = saveAuth({ url, token });
2875
+ return s.stop(`Signed in. ${check.products} product(s) in this workspace. Saved to ${import_picocolors4.default.dim(file2)}`), { url, token };
1470
2876
  }
1471
- s.stop(import_picocolors3.default.red(check.message ?? "That did not work."));
2877
+ s.stop(import_picocolors4.default.red(check.message ?? "That did not work."));
1472
2878
  }
1473
2879
  }
1474
- function printDetection(d) {
1475
- let stack = d.stack.map((s) => `${import_picocolors3.default.bold(s.name)} ${import_picocolors3.default.dim(s.kind)}`).join(`
1476
- `) || import_picocolors3.default.dim("Nothing recognised");
1477
- Me(stack, "Your stack");
1478
- let found = d.sources.filter((s) => s.status === "found");
2880
+ async function printDetection(d3, facts) {
2881
+ Me(stackRows(d3, facts).join(`
2882
+ `), "Your stack"), await pace(motion, 150);
2883
+ let found = d3.sources.filter((s) => s.status === "found");
1479
2884
  if (found.length)
1480
- Me(found.map((s) => `${import_picocolors3.default.green("●")} ${import_picocolors3.default.bold(s.label)} ${import_picocolors3.default.dim(s.evidence.join(", "))}`).join(`
1481
- `), "Already in your code, ready to connect");
1482
- let possible = d.sources.filter((s) => s.status === "possible");
2885
+ Me(found.map((s) => `${import_picocolors4.default.green("●")} ${import_picocolors4.default.bold(s.label)} ${import_picocolors4.default.dim(s.evidence.join(", "))}`).join(`
2886
+ `), "Already in your code, ready to connect"), await pace(motion, 150);
2887
+ let possible = d3.sources.filter((s) => s.status === "possible");
1483
2888
  if (possible.length)
1484
- Me(possible.map((s) => `${import_picocolors3.default.yellow("?")} ${import_picocolors3.default.bold(s.label)} ${import_picocolors3.default.dim(`only ${s.evidence.join(", ")}`)}`).join(`
1485
- `), "Maybe in use, your agent will check");
1486
- for (let gap of d.gaps)
2889
+ Me(possible.map((s) => `${import_picocolors4.default.yellow("?")} ${import_picocolors4.default.bold(s.label)} ${import_picocolors4.default.dim(`only ${s.evidence.join(", ")}`)}`).join(`
2890
+ `), "Maybe in use, your agent will check"), await pace(motion, 150);
2891
+ for (let gap of d3.gaps)
1487
2892
  M2.warn(gap);
1488
2893
  }
2894
+ async function scanWithProgress() {
2895
+ let s = Y2();
2896
+ s.start("Reading this folder");
2897
+ let steps = [], facts = scanProject(cwd, (step) => steps.push(step));
2898
+ await pace(motion, 400), s.stop("Read this folder");
2899
+ for (let step of steps)
2900
+ M2.success(step.detail ? `${step.label}
2901
+ ${import_picocolors4.default.dim(step.detail)}` : step.label), await pace(motion, 120);
2902
+ let detection = detectStack(facts);
2903
+ return { facts, detection };
2904
+ }
1489
2905
  function selfCommand() {
1490
2906
  let self = process.argv[1] ?? "";
1491
- return self.includes("node_modules") || !self ? ["npx", "-y", "lopebase", "mcp"] : [process.execPath, self, "mcp"];
2907
+ if (self && !self.includes("node_modules"))
2908
+ return [process.execPath, self, "mcp"];
2909
+ return process.platform === "win32" ? ["cmd", "/c", "npx", "-y", "lopebase", "mcp"] : ["npx", "-y", "lopebase", "mcp"];
1492
2910
  }
1493
2911
  function registerMcp(agent) {
1494
2912
  if (!agent.mcpAdd)
1495
2913
  return !1;
1496
- let argv = agent.mcpAdd(selfCommand()), res = spawnSync2(agent.bin, argv, {
2914
+ let argv = agent.mcpAdd(selfCommand()), res = spawnAgentSync(agent.bin, argv, {
1497
2915
  stdio: "pipe",
1498
2916
  cwd,
1499
2917
  encoding: "utf8"
@@ -1501,29 +2919,40 @@ function registerMcp(agent) {
1501
2919
  return res.status === 0 || /already exists/i.test(`${res.stderr}${res.stdout}`);
1502
2920
  }
1503
2921
  async function init() {
1504
- console.clear(), Ie(`${import_picocolors3.default.bgYellow(import_picocolors3.default.black(" LopeBase "))} set up this project`);
1505
- let s = Y2();
1506
- s.start("Scanning this folder");
1507
- let facts = scanProject(cwd), detection = detectStack(facts);
1508
- s.stop(`Scanned ${facts.dependencies.length} dependencies and ${facts.files.length} config files`), printDetection(detection);
1509
- let auth = loadAuth() ?? await login();
1510
- if (!(await checkToken(auth)).ok)
1511
- M2.warn("Your saved token no longer works. Let's get a new one."), await login();
2922
+ 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(`
2923
+ `), "Here is what happens"), await pace(motion, 300), M2.step(stepTitle(1));
2924
+ let { facts, detection } = await scanWithProgress();
2925
+ await printDetection(detection, facts), M2.step(stepTitle(2));
2926
+ let auth = loadAuth();
2927
+ if (auth) {
2928
+ let s2 = Y2();
2929
+ s2.start(`Checking your connection to ${auth.url}`);
2930
+ let check = await checkToken(auth);
2931
+ if (check.ok)
2932
+ s2.stop(`Connected to ${import_picocolors4.default.bold(auth.url)}. ${check.products} ${check.products === 1 ? "product" : "products"} so far.`);
2933
+ else
2934
+ s2.stop(import_picocolors4.default.yellow("Your saved connection no longer works.")), auth = null;
2935
+ }
2936
+ auth ??= await login(), M2.step(stepTitle(3));
1512
2937
  let productName = guard(await he({
1513
2938
  message: "What is this product called?",
1514
2939
  initialValue: facts.name?.replace(/^@[^/]+\//, "") ?? ""
1515
2940
  })), chosen = guard(await fe({
1516
- message: "What should we connect? (space to toggle)",
2941
+ message: "What should we connect? (space to toggle, enter to go on)",
1517
2942
  options: detection.sources.map((src) => ({
1518
2943
  value: src.label,
1519
- label: src.status === "found" ? src.label : src.status === "possible" ? `${src.label} ${import_picocolors3.default.dim("(maybe in use, we'll check)")}` : `${src.label} ${import_picocolors3.default.dim("(not set up yet, we'll help)")}`,
2944
+ 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)")}`,
1520
2945
  hint: src.why
1521
2946
  })),
1522
- initialValues: detection.sources.filter((src) => src.status === "found" || src.source === "gsc").map((src) => src.label),
2947
+ initialValues: detection.sources.filter((src) => src.status === "found" || src.source === "gsc" || src.source === "uptime").map((src) => src.label),
1523
2948
  required: !1
1524
- })), prompt = onboardingPrompt({ facts, detection, chosen, productName }), agents = installedAgents();
2949
+ }));
2950
+ M2.step(stepTitle(4));
2951
+ let prompt = onboardingPrompt({ facts, detection, chosen, productName }), agents = installedAgents();
1525
2952
  if (!agents.length || flag("print")) {
1526
- Me(prompt, "Paste this into your coding agent"), Se(`Add the MCP server first: ${import_picocolors3.default.cyan("npx lopebase mcp")} (stdio)`);
2953
+ if (!agents.length)
2954
+ M2.info("No coding agent found on this computer (Claude Code, Codex, and others). Paste the prompt below into the one you use.");
2955
+ Me(prompt, "Paste this into your coding agent"), Se(`Add the MCP server first: ${import_picocolors4.default.cyan("npx lopebase mcp")} (stdio)`);
1527
2956
  return;
1528
2957
  }
1529
2958
  let agentId = guard(await ve({
@@ -1537,29 +2966,66 @@ async function init() {
1537
2966
  Me(prompt, "Paste this into your coding agent"), Se("Done.");
1538
2967
  return;
1539
2968
  }
1540
- let agent = agents.find((a) => a.id === agentId);
1541
- if (registerMcp(agent))
1542
- M2.success(`Added LopeBase tools to ${agent.label} for this folder.`);
2969
+ let agent = agents.find((a) => a.id === agentId), s = Y2();
2970
+ s.start(`Adding the LopeBase tools to ${agent.label}`);
2971
+ let registered = registerMcp(agent);
2972
+ if (await pace(motion, 300), registered)
2973
+ s.stop(`Added the LopeBase tools to ${agent.label} for this folder.`);
1543
2974
  else
1544
- M2.warn(`Add an MCP server to ${agent.label} that runs: npx -y lopebase mcp`);
1545
- Se(`Handing over to ${agent.label}. You stay in the loop for anything that needs you.`), spawn(agent.bin, agent.args(prompt), { stdio: "inherit", cwd }).on("exit", (code) => process.exit(code ?? 0));
2975
+ s.stop(import_picocolors4.default.yellow(`Add an MCP server to ${agent.label} that runs: npx -y lopebase mcp`));
2976
+ Me([
2977
+ `${import_picocolors4.default.bold(agent.label)} will now:`,
2978
+ ` ${import_picocolors4.default.dim("1.")} add ${import_picocolors4.default.bold(productName || "this product")} to your workspace`,
2979
+ ` ${import_picocolors4.default.dim("2.")} connect ${chosen.length ? chosen.join(", ") : "what it recommends"}`,
2980
+ ` ${import_picocolors4.default.dim("3.")} stop and ask you for any sign-in, key, or yes`,
2981
+ "",
2982
+ import_picocolors4.default.dim("You can follow along in your workspace as each one turns green.")
2983
+ ].join(`
2984
+ `), "Handing over"), Se(`Starting ${agent.label}. Press Ctrl+C any time to stop.`);
2985
+ let child = spawnAgent(agent.bin, agent.args(prompt), {
2986
+ stdio: "inherit",
2987
+ cwd
2988
+ });
2989
+ child.on("error", (e2) => bail(`Could not start ${agent.label}: ${e2.message}`)), child.on("exit", (code) => process.exit(code ?? 0));
1546
2990
  }
1547
2991
  async function main() {
1548
2992
  switch (command) {
1549
2993
  case "init":
1550
2994
  return init();
1551
2995
  case "login":
1552
- return Ie(`${import_picocolors3.default.bgYellow(import_picocolors3.default.black(" LopeBase "))} sign in`), await login(), Se("Ready. Run lopebase in any project.");
2996
+ 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.");
1553
2997
  case "scan": {
1554
2998
  let facts = scanProject(cwd), detection = detectStack(facts);
1555
2999
  if (flag("json"))
1556
3000
  return console.log(JSON.stringify({ facts, detection }, null, 2));
1557
- return printDetection(detection);
3001
+ 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.`);
1558
3002
  }
1559
3003
  case "prompt": {
1560
3004
  let facts = scanProject(cwd), detection = detectStack(facts);
1561
3005
  return console.log(onboardingPrompt({ facts, detection, chosen: [] }));
1562
3006
  }
3007
+ case "adapter": {
3008
+ let sub = rest[0];
3009
+ if (sub === "secret")
3010
+ return process.stdout.write(`${newSigningSecret()}
3011
+ `);
3012
+ if (sub === "check" && rest[1]) {
3013
+ let envName = option("secret-env") ?? "LOPEBASE_SIGNING_SECRET", secret = process.env[envName]?.trim(), token = process.env.LOPEBASE_ADAPTER_TOKEN?.trim();
3014
+ if (!secret && !token)
3015
+ console.error(`Set ${envName} to the adapter's signing secret (or LOPEBASE_ADAPTER_TOKEN for an older bearer adapter).`), process.exit(1);
3016
+ console.log(`Checking ${rest[1]}`);
3017
+ let res = await checkAdapter({
3018
+ baseUrl: rest[1],
3019
+ secret,
3020
+ token: secret ? void 0 : token,
3021
+ log: (l2) => console.log(l2.startsWith("FAIL") ? import_picocolors4.default.red(l2) : import_picocolors4.default.green(l2))
3022
+ });
3023
+ console.log(res.failed.length ? import_picocolors4.default.red(`${res.failed.length} failed, ${res.passed} passed`) : import_picocolors4.default.green(`All ${res.passed} checks passed. Ready to connect.`)), process.exit(res.failed.length ? 1 : 0);
3024
+ }
3025
+ return console.log(`Usage:
3026
+ lopebase adapter secret print a new signing secret
3027
+ lopebase adapter check <base-url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)`), process.exit(sub ? 1 : 0);
3028
+ }
1563
3029
  case "mcp": {
1564
3030
  let auth = loadAuth();
1565
3031
  if (!auth)
@@ -1567,7 +3033,7 @@ async function main() {
1567
3033
  return runMcpProxy(auth);
1568
3034
  }
1569
3035
  default:
1570
- console.log("Usage: lopebase [init|login|scan|prompt|mcp]"), process.exit(command === "--help" || command === "-h" ? 0 : 1);
3036
+ console.log(HELP), process.exit(command === "--help" || command === "-h" || command === "help" ? 0 : 1);
1571
3037
  }
1572
3038
  }
1573
3039
  main();