cortad 0.1.0 → 0.1.1

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 +29 -3
  2. package/package.json +8 -1
package/local.mjs CHANGED
@@ -32,12 +32,19 @@ const say = (line) => console.log(`cortad ${line}`);
32
32
  const fail = (line) => { console.error(`cortad ${line}`); process.exit(1); };
33
33
 
34
34
  const explain = argv.includes("--explain");
35
+ // What is happening right now, on one line that rewrites itself. npx spends its own seconds fetching
36
+ // this package before anything here runs, and the first thing we printed used to be after the whole
37
+ // upload: a minute or more of a cursor sitting still, which reads as nothing happening.
38
+ const step = (line) => { if (process.stdout.isTTY) process.stdout.write(`\rcortad ${line}\x1b[K`); };
39
+ const clearStep = () => { if (process.stdout.isTTY) process.stdout.write("\r\x1b[K"); };
40
+ const stepDone = (line) => { clearStep(); say(line); };
35
41
  if (!explain && !/^[A-Z0-9]{8}$/.test(code)) fail("usage: npx cortad <code from the connect screen> [--port N] [--start \"cmd\"] | npx cortad --explain");
36
42
  // Where Brainsless is. The host is not on the command line: a code cannot point at an impostor.
37
43
  const origin = new URL(process.env.CORTAD_ORIGIN || "https://cortad.com");
38
44
  // Ours, and only ours. brainsless.com is the same service under its earlier name and stays trusted
39
45
  // while people still hold links to it.
40
- const OURS = ["cortad.com", "brainsless.com"];
46
+ // The last is our own Pages project: its subdomains are our branch deploys, staging among them.
47
+ const OURS = ["cortad.com", "brainsless.com", "brainsless-frontend.pages.dev"];
41
48
  const trusted = (origin.protocol === "https:" && OURS.some((host) => origin.hostname === host || origin.hostname.endsWith(`.${host}`)))
42
49
  || origin.hostname === "localhost" || origin.hostname === "127.0.0.1";
43
50
  if (!trusted) fail(`refusing: ${origin.host} is not Cortad.`);
@@ -304,6 +311,7 @@ async function startApp() {
304
311
  const lifted = liftedLimits(envFiles);
305
312
  if (Object.keys(lifted).length) say(`your own request limits are raised for this session: ${Object.keys(lifted).join(", ")}`);
306
313
  launched = { cmd, lifted };
314
+ step(`starting your app: ${cmd}`);
307
315
  const up = await launch(180_000);
308
316
  if (up.port) return { port: up.port, cmd, lifted: Object.keys(lifted) };
309
317
  // Already running: a second start dies on the port the first one holds. The one that is running
@@ -417,6 +425,8 @@ async function stopApp(pid) {
417
425
  }
418
426
 
419
427
  // ---- go
428
+ say("starting");
429
+ step("reading this folder");
420
430
  const envFiles = [];
421
431
  const files = [];
422
432
  walk(root, 0, files, envFiles, 0);
@@ -464,19 +474,34 @@ key = attach.data.key;
464
474
 
465
475
  const list = join(work, "files.txt");
466
476
  writeFileSync(list, files.join("\n") + "\n");
477
+ // Which code this is, from what the files say rather than from the archive: gzip stamps the time into
478
+ // every archive, so the same folder packed twice never matched and every reconnect bought a whole new
479
+ // read of code that had not changed, emptying the page's counts and its price while it ran.
480
+ const treeDigest = (() => {
481
+ const h = createHash("sha256");
482
+ for (const rel of [...files].sort()) {
483
+ let body = Buffer.alloc(0);
484
+ try { body = readFileSync(join(root, rel)); } catch { continue; }
485
+ h.update(rel).update("\0").update(createHash("sha256").update(body).digest("hex")).update("\n");
486
+ }
487
+ return h.digest("hex");
488
+ })();
467
489
  const archive = join(work, "tree.tgz");
490
+ step(`packing ${files.length} files`);
468
491
  await exec("tar", ["-czf", archive, "-C", root, "-T", list]);
469
492
  const bytes = readFileSync(archive);
470
493
  const PART = 1_500_000;
471
494
  let resumed = false;
495
+ const parts = Math.max(1, Math.ceil(bytes.length / PART));
472
496
  for (let off = 0; off < bytes.length; off += PART) {
473
497
  const last = off + PART >= bytes.length;
474
- const put = await call("PUT", `/local/${box}/tree?last=${last ? 1 : 0}`, bytes.subarray(off, off + PART), { raw: true, timeoutMs: 120_000 });
498
+ step(parts > 1 ? `sending your code, part ${Math.floor(off / PART) + 1} of ${parts}` : "sending your code");
499
+ const put = await call("PUT", `/local/${box}/tree?last=${last ? 1 : 0}${last ? `&digest=${treeDigest}` : ""}`, bytes.subarray(off, off + PART), { raw: true, timeoutMs: 120_000 });
475
500
  if (!put.ok) fail(put.data?.error ?? `upload failed (${put.status})`);
476
501
  if (last) resumed = put.data?.resumed === true;
477
502
  }
478
503
  // Unchanged code has already been read: coming back says so instead of claiming a second read.
479
- say(resumed ? "connected · your code is unchanged, picking up where you left off" : `connected · reading your code (${files.length} files, ${Math.round(bytes.length / 1024)} KB)`);
504
+ stepDone(resumed ? "connected · your code is unchanged, picking up where you left off" : `connected · reading your code (${files.length} files, ${Math.round(bytes.length / 1024)} KB)`);
480
505
 
481
506
  lock = await makeLock({ root, work });
482
507
  if (lock && !lockHolds(lock, root)) lock = null;
@@ -517,6 +542,7 @@ async function appLife() {
517
542
  say("saw your change, starting your app again");
518
543
  }
519
544
  const told = await announce();
545
+ clearStep();
520
546
  if (!told.ok) fail(told.data?.error ?? `could not register your app (${told.status})`);
521
547
  say(`your app is answering on port ${app.port}${app.cmd ? ` · ${app.cmd}` : ""}`);
522
548
  say("leave this open. Go back to the browser; Ctrl-C disconnects.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Runs Cortad test conversations against the app on your machine. Env values never leave it. Zero dependencies, no install scripts.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"
@@ -25,5 +25,12 @@
25
25
  "homepage": "https://cortad.com",
26
26
  "publishConfig": {
27
27
  "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/brainsless/cortad.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/brainsless/cortad/issues"
28
35
  }
29
36
  }