cortad 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/local.mjs +58 -1
  2. package/package.json +1 -1
package/local.mjs CHANGED
@@ -346,6 +346,8 @@ async function startApp() {
346
346
  const wanted = Number(flag("--port"));
347
347
  if (wanted) {
348
348
  if (!(await answers(wanted))) fail(`nothing is answering on port ${wanted}. Start your app first, then run this again.`);
349
+ const took = await takeOver(wanted);
350
+ if (took) return took;
349
351
  say("your app was already running, so its request limits stay as they are; if it answers 429, stop it and run this without --port and they are raised for the session");
350
352
  return { port: wanted, cmd: null };
351
353
  }
@@ -366,7 +368,13 @@ async function startApp() {
366
368
  if (up.exited !== null && /EADDRINUSE|address already in use|port.{0,40}(?:in use|already used|is taken|unavailable)/i.test(up.tail)) {
367
369
  const ports = [...up.tail.matchAll(/(?::|port\s*[:=]?\s*)(\d{4,5})\b/gi)].map((m) => Number(m[1]));
368
370
  for (const port of new Set(ports)) {
369
- if (await answers(port)) { launched = null; child = null; say(`your app is already running on port ${port}, so that one is used`); return { port, cmd: null }; }
371
+ if (await answers(port)) {
372
+ launched = null; child = null;
373
+ const took = await takeOver(port);
374
+ if (took) return took;
375
+ say(`your app is already running on port ${port}, so that one is used; its request limits stay as they are`);
376
+ return { port, cmd: null };
377
+ }
370
378
  }
371
379
  }
372
380
  if (up.tail) console.error(up.tail);
@@ -374,6 +382,55 @@ async function startApp() {
374
382
  return { port: null, said: up.tail, why: `${up.exited !== null ? "your app stopped before it answered" : "your app did not answer within three minutes"}; what it said is above.${wrongNode}` };
375
383
  }
376
384
 
385
+ // An app the person started keeps the limits it started with, and a run needs more than a person
386
+ // uses in a month. With their yes, the command stops that app and starts it the way it would have,
387
+ // limits raised for the session. Only in a terminal, only when the code reads a limit, and only
388
+ // when the command knows how to start the app.
389
+ async function takeOver(port) {
390
+ const lifted = liftedLimits(envFiles, files.map((f) => join(root, f)));
391
+ const names = Object.keys(lifted);
392
+ if (!names.length || !process.stdin.isTTY) return null;
393
+ const plan = startPlan({ root, typed: flag("--start"), onPath });
394
+ if (!plan?.cmd) return null;
395
+ const pid = await listenerOn(port);
396
+ if (!pid) return null;
397
+ const answer = await ask(`your app is running with its own request limits (${names.join(", ")}). Restart it with them raised for this session? [Y/n] `);
398
+ if (answer && !/^y(?:es)?$/i.test(answer)) return null;
399
+ const top = await supervisorOf(pid);
400
+ step(`stopping your app (pid ${top}) to start it with higher limits`);
401
+ await stopApp(top);
402
+ for (let i = 0; i < 50 && (await answers(port)); i++) await new Promise((r) => setTimeout(r, 200));
403
+ if (await answers(port)) { say("your app did not stop, so it is used as it is"); return null; }
404
+ appDir = plan.cwd ?? root;
405
+ pinned = pinnedNode();
406
+ if (plan.within) say(`your app is in ${plan.within}, started there with: ${plan.cmd}`);
407
+ say(`higher request limits for this session: ${names.join(", ")}`);
408
+ launched = { cmd: plan.cmd, lifted };
409
+ step(`starting your app: ${plan.cmd}`);
410
+ const up = await launch(180_000);
411
+ if (up.port) return { port: up.port, cmd: plan.cmd, lifted: names };
412
+ if (up.tail) console.error(up.tail);
413
+ fail("your app did not come back after the restart. Start it yourself, then run this again with --port.");
414
+ }
415
+ // The process listening on a port.
416
+ async function listenerOn(port) {
417
+ try { return Number((await exec("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"])).stdout.trim().split("\n")[0]) || null; } catch { return null; }
418
+ }
419
+ // The top of the chain that runs the app: nodemon, npm, the sh -c under it. Climbs from the listener
420
+ // while the parent is a runner, never into the person's own shell or terminal.
421
+ const RUNNER = /^(?:\S*\/)?(?:node|npm|npx|pnpm|yarn|bun|deno|python[\d.]*|uvicorn|gunicorn|flask|tsx|ts-node|nodemon|concurrently|pm2)(?:\s|$)|^(?:\/bin\/)?sh -c\b/;
422
+ async function supervisorOf(pid) {
423
+ const rows = (await exec("ps", ["-axo", "pid=,ppid=,args="])).stdout.trim().split("\n").map((l) => l.trim());
424
+ const table = new Map(rows.map((l) => { const m = /^(\d+)\s+(\d+)\s+(.*)$/.exec(l); return m ? [Number(m[1]), { ppid: Number(m[2]), args: m[3] }] : [0, null]; }));
425
+ let top = pid;
426
+ for (let i = 0; i < 8; i++) {
427
+ const parent = table.get(table.get(top)?.ppid ?? 0);
428
+ if (!parent || !RUNNER.test(parent.args)) break;
429
+ top = table.get(top).ppid;
430
+ }
431
+ return top;
432
+ }
433
+
377
434
  // Your app, started the way you start it, and watched until one of its own ports answers.
378
435
  let launched = null;
379
436
  // The Node this project pins, when the shell that ran this command has another. Strapi refuses
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"