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.
- package/LICENSE +21 -0
- package/README.md +116 -20
- package/dist/lopebase.js +1718 -252
- 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
|
|
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
|
|
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:
|
|
596
|
-
n >=
|
|
597
|
-
let $ = a <
|
|
598
|
-
return
|
|
599
|
-
let
|
|
600
|
-
return
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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)} ${
|
|
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 = (
|
|
645
|
-
let s =
|
|
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} ${
|
|
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
|
|
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 `${
|
|
1021
|
+
return `${r2}${import_picocolors2.default.gray(o)} ${n(this.options[this.cursor], "selected")}`;
|
|
664
1022
|
case "cancel":
|
|
665
|
-
return `${
|
|
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 `${
|
|
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 = (
|
|
677
|
-
let s =
|
|
678
|
-
return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${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(
|
|
681
|
-
if (this.required &&
|
|
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
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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
|
|
1075
|
+
let r2 = `
|
|
718
1076
|
${t}
|
|
719
1077
|
`.split(`
|
|
720
|
-
`), i = S2(n).length, s = Math.max(
|
|
721
|
-
let $ = S2(
|
|
722
|
-
return
|
|
723
|
-
}, 0), i) + 2, c =
|
|
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
|
|
1101
|
+
let r2 = [`${import_picocolors2.default.gray(o)}`];
|
|
744
1102
|
if (t) {
|
|
745
1103
|
let [i, ...s] = t.split(`
|
|
746
1104
|
`);
|
|
747
|
-
|
|
1105
|
+
r2.push(`${n} ${i}`, ...s.map((c) => `${import_picocolors2.default.gray(o)} ${c}`));
|
|
748
1106
|
}
|
|
749
|
-
process.stdout.write(`${
|
|
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"],
|
|
767
|
-
let
|
|
768
|
-
a &&
|
|
769
|
-
},
|
|
770
|
-
process.on("uncaughtExceptionMonitor",
|
|
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",
|
|
773
|
-
},
|
|
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
|
|
1136
|
+
let m2 = $2.split(`
|
|
779
1137
|
`);
|
|
780
|
-
process.stdout.write(import_sisteransi2.cursor.move(-999,
|
|
781
|
-
},
|
|
782
|
-
let
|
|
783
|
-
return
|
|
784
|
-
},
|
|
785
|
-
a = !0, s = fD(),
|
|
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
|
|
788
|
-
|
|
789
|
-
if (i &&
|
|
1145
|
+
let h2 = 0, w2 = 0;
|
|
1146
|
+
j2(), c = setInterval(() => {
|
|
1147
|
+
if (i && l2 === $2)
|
|
790
1148
|
return;
|
|
791
|
-
|
|
792
|
-
let
|
|
1149
|
+
B2(), $2 = l2;
|
|
1150
|
+
let I2 = import_picocolors2.default.magenta(n[h2]);
|
|
793
1151
|
if (i)
|
|
794
|
-
process.stdout.write(`${
|
|
1152
|
+
process.stdout.write(`${I2} ${l2}...`);
|
|
795
1153
|
else if (t === "timer")
|
|
796
|
-
process.stdout.write(`${
|
|
1154
|
+
process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
|
|
797
1155
|
else {
|
|
798
|
-
let
|
|
799
|
-
process.stdout.write(`${
|
|
1156
|
+
let z2 = ".".repeat(Math.floor(w2)).slice(0, 3);
|
|
1157
|
+
process.stdout.write(`${I2} ${l2}${z2}`);
|
|
800
1158
|
}
|
|
801
|
-
|
|
802
|
-
},
|
|
803
|
-
},
|
|
804
|
-
a = !1, clearInterval(c),
|
|
805
|
-
let
|
|
806
|
-
|
|
807
|
-
`) : process.stdout.write(`${
|
|
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:
|
|
811
|
-
|
|
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
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
1493
|
+
args: (p2) => [p2]
|
|
861
1494
|
}
|
|
862
1495
|
];
|
|
863
1496
|
function isInstalled(bin) {
|
|
864
|
-
|
|
865
|
-
|
|
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((
|
|
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((
|
|
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: {
|
|
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: [
|
|
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: [
|
|
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: {
|
|
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
|
-
{
|
|
964
|
-
|
|
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: "
|
|
973
|
-
rule: { deps: [/^@
|
|
1742
|
+
name: "Qwik",
|
|
1743
|
+
rule: { deps: [/^@builder\.io\/qwik-city$/] }
|
|
974
1744
|
},
|
|
975
|
-
{
|
|
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: [
|
|
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: {
|
|
1822
|
+
rule: {
|
|
1823
|
+
files: [file("netlify.toml")],
|
|
1824
|
+
deps: [/^@netlify\//, /^@astrojs\/netlify$/]
|
|
1825
|
+
}
|
|
990
1826
|
},
|
|
991
|
-
{ kind: "hosting", name: "Fly.io", rule: { files: [
|
|
1827
|
+
{ kind: "hosting", name: "Fly.io", rule: { files: [file("fly.toml")] } },
|
|
992
1828
|
{
|
|
993
|
-
kind: "
|
|
994
|
-
name: "
|
|
995
|
-
rule: {
|
|
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: [
|
|
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: "
|
|
1005
|
-
rule: {
|
|
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: "
|
|
1010
|
-
rule: { deps: [/^
|
|
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: "
|
|
1022
|
-
rule: { deps: [/^
|
|
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: "
|
|
1027
|
-
rule: { deps: [/^@
|
|
1957
|
+
name: "Paddle",
|
|
1958
|
+
rule: { deps: [/^@paddle\//], env: [/^PADDLE_/] }
|
|
1028
1959
|
},
|
|
1029
1960
|
{ kind: "auth", name: "Clerk", rule: { deps: [/^@clerk\//] } },
|
|
1030
|
-
{
|
|
1031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1105
|
-
if (typeof
|
|
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:
|
|
1109
|
-
userCode:
|
|
1110
|
-
verifyUrl: String(
|
|
1111
|
-
interval: Number(
|
|
1112
|
-
expiresIn: Number(
|
|
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((
|
|
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
|
|
2142
|
+
let body2;
|
|
1123
2143
|
try {
|
|
1124
|
-
|
|
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
|
|
2155
|
+
if (typeof body2.access_token === "string")
|
|
1136
2156
|
return {
|
|
1137
2157
|
ok: !0,
|
|
1138
|
-
token:
|
|
1139
|
-
url: typeof
|
|
1140
|
-
scope: String(
|
|
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 (
|
|
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(
|
|
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 (
|
|
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(
|
|
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"],
|
|
1276
|
-
"
|
|
1277
|
-
"
|
|
1278
|
-
"
|
|
1279
|
-
"
|
|
1280
|
-
"
|
|
1281
|
-
"
|
|
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
|
-
"
|
|
1285
|
-
"
|
|
1286
|
-
"
|
|
1287
|
-
"
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
|
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
|
|
1305
|
-
if (!
|
|
2418
|
+
let g2 = path2.join(root, group);
|
|
2419
|
+
if (!isDir(g2))
|
|
1306
2420
|
continue;
|
|
1307
|
-
if (
|
|
1308
|
-
dirs.
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
}
|
|
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((
|
|
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
|
|
1330
|
-
if (
|
|
1331
|
-
out.push(
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
|
1345
|
-
let
|
|
1346
|
-
if (
|
|
1347
|
-
for (let n of Object.keys(
|
|
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
|
|
1352
|
-
|
|
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(
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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),
|
|
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" ? "
|
|
1393
|
-
|
|
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
|
|
1413
|
-
return { ok: !0, products: JSON.parse(
|
|
1414
|
-
} catch (
|
|
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(
|
|
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:
|
|
1426
|
-
validate: (
|
|
1427
|
-
}))
|
|
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: ${
|
|
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(
|
|
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
|
|
1443
|
-
return s.stop(`Connected (${res.scope}). Saved to ${
|
|
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(
|
|
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 ${
|
|
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(
|
|
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: (
|
|
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
|
|
1469
|
-
return s.stop(`Signed in. ${check.products} product(s) in this workspace. Saved to ${
|
|
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(
|
|
2877
|
+
s.stop(import_picocolors4.default.red(check.message ?? "That did not work."));
|
|
1472
2878
|
}
|
|
1473
2879
|
}
|
|
1474
|
-
function printDetection(
|
|
1475
|
-
|
|
1476
|
-
`)
|
|
1477
|
-
|
|
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) => `${
|
|
1481
|
-
`), "Already in your code, ready to connect");
|
|
1482
|
-
let 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) => `${
|
|
1485
|
-
`), "Maybe in use, your agent will check");
|
|
1486
|
-
for (let gap of
|
|
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
|
-
|
|
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 =
|
|
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(`${
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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} ${
|
|
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
|
-
}))
|
|
2949
|
+
}));
|
|
2950
|
+
M2.step(stepTitle(4));
|
|
2951
|
+
let prompt = onboardingPrompt({ facts, detection, chosen, productName }), agents = installedAgents();
|
|
1525
2952
|
if (!agents.length || flag("print")) {
|
|
1526
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1545
|
-
|
|
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(`${
|
|
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(
|
|
3036
|
+
console.log(HELP), process.exit(command === "--help" || command === "-h" || command === "help" ? 0 : 1);
|
|
1571
3037
|
}
|
|
1572
3038
|
}
|
|
1573
3039
|
main();
|