lakebed 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,8 +120,8 @@ queries: {
120
120
  ```sh
121
121
  lakebed new [name] [--template todo] [--no-git]
122
122
  lakebed create [name] [--template todo] [--no-git]
123
- lakebed dev <capsule-dir> [--port 3000]
124
- lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
123
+ lakebed dev [capsule-dir] [--port 3000]
124
+ lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
125
125
  lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
126
126
  lakebed claim [capsule-dir] [--api <url>] [--json]
127
127
  lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lakebed",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Agent-native CLI and runtime for building and deploying Lakebed capsules.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { execFile } from "node:child_process";
3
3
  import { createServer } from "node:http";
4
4
  import { existsSync, realpathSync } from "node:fs";
5
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { promisify } from "node:util";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -38,13 +38,13 @@ function usage() {
38
38
  Usage:
39
39
  lakebed new [name] [--template todo] [--no-git]
40
40
  lakebed create [name] [--template todo] [--no-git]
41
- lakebed dev <capsule-dir> [--port 3000]
42
- lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
41
+ lakebed dev [capsule-dir] [--port 3000]
42
+ lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
43
43
  lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
44
44
  lakebed claim [capsule-dir] [--api <url>] [--json]
45
45
  lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
46
46
  lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
47
- lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
47
+ lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
48
48
  lakebed auth as <name>
49
49
  lakebed auth reset
50
50
  lakebed db list [deploy-id-or-url] [--port 3000]
@@ -62,12 +62,27 @@ function readArg(args, name, fallback) {
62
62
  return args[index + 1] ?? fallback;
63
63
  }
64
64
 
65
+ const optionsWithValues = new Set([
66
+ "--api",
67
+ "--app-base-domain",
68
+ "--base-port",
69
+ "--count",
70
+ "--out",
71
+ "--port",
72
+ "--public-root-url",
73
+ "--target",
74
+ "--template",
75
+ "--ttl"
76
+ ]);
77
+
65
78
  function positionals(args) {
66
79
  const values = [];
67
80
  for (let index = 0; index < args.length; index += 1) {
68
81
  const value = args[index];
69
82
  if (value.startsWith("--")) {
70
- index += 1;
83
+ if (optionsWithValues.has(value)) {
84
+ index += 1;
85
+ }
71
86
  continue;
72
87
  }
73
88
 
@@ -92,7 +107,7 @@ function hasFlag(args, name) {
92
107
 
93
108
  function resolveCapsuleDir(value) {
94
109
  if (!value) {
95
- return resolve(root, "examples/todo");
110
+ return root;
96
111
  }
97
112
 
98
113
  return isAbsolute(value) ? value : resolve(root, value);
@@ -356,6 +371,32 @@ function sendJson(ws, message) {
356
371
  ws.send(JSON.stringify(message));
357
372
  }
358
373
 
374
+ async function capsuleFileFingerprint(rootDir, dir = rootDir, entries = []) {
375
+ const dirEntries = await readdir(dir, { withFileTypes: true });
376
+
377
+ for (const entry of dirEntries) {
378
+ if (entry.name === "node_modules" || entry.name === ".lakebed" || entry.name === ".DS_Store") {
379
+ continue;
380
+ }
381
+
382
+ const absolutePath = join(dir, entry.name);
383
+ if (entry.isDirectory()) {
384
+ await capsuleFileFingerprint(rootDir, absolutePath, entries);
385
+ continue;
386
+ }
387
+
388
+ if (!entry.isFile()) {
389
+ continue;
390
+ }
391
+
392
+ const info = await stat(absolutePath);
393
+ const path = relative(rootDir, absolutePath).split(sep).join("/");
394
+ entries.push(`${path}:${info.size}:${info.mtimeMs}`);
395
+ }
396
+
397
+ return entries.sort().join("\n");
398
+ }
399
+
359
400
  function createContext({ stateCell, auth, logs, env }) {
360
401
  return {
361
402
  auth,
@@ -402,25 +443,53 @@ export async function startDevServer({
402
443
  shooBaseUrl = shooBaseUrlFromEnv()
403
444
  } = {}) {
404
445
  const resolvedCapsuleDir = resolveCapsuleDir(capsuleDir);
405
- const built = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
446
+ let currentBuild = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
406
447
  const defaultAuth = await readAuth();
407
- const stateCell = new StateCell(built.app.schema);
448
+ const stateCell = new StateCell(currentBuild.app.schema);
408
449
  const logs = new LogBuffer();
409
450
  const subscriptions = new Map();
451
+ let fileFingerprint = sourceStore ? "" : await capsuleFileFingerprint(resolvedCapsuleDir);
452
+ let rebuildTimer = null;
453
+ let rebuildPromise = Promise.resolve();
454
+
455
+ async function rebuild() {
456
+ try {
457
+ const nextBuild = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
458
+ currentBuild = nextBuild;
459
+ stateCell.updateSchema(nextBuild.app.schema);
460
+ logs.append("info", "dev server rebuilt capsule", { capsuleDir: resolvedCapsuleDir });
461
+ for (const client of wss.clients) {
462
+ sendJson(client, { op: "refresh" });
463
+ }
464
+ } catch (error) {
465
+ logs.append("error", "dev server rebuild failed", { error: error instanceof Error ? error.message : String(error) });
466
+ }
467
+ }
468
+
469
+ function scheduleRebuild() {
470
+ if (rebuildTimer) {
471
+ clearTimeout(rebuildTimer);
472
+ }
473
+
474
+ rebuildTimer = setTimeout(() => {
475
+ rebuildTimer = null;
476
+ rebuildPromise = rebuildPromise.then(rebuild, rebuild);
477
+ }, 100);
478
+ }
410
479
 
411
480
  const server = createServer(async (req, res) => {
412
481
  try {
413
482
  const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
414
483
 
415
484
  if (requestUrl.pathname === "/" || requestUrl.pathname === "/index.html" || requestUrl.pathname === "/auth/callback") {
416
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
417
- res.end(html(built.app.name ?? "Lakebed Capsule", { shooBaseUrl }));
485
+ res.writeHead(200, { "Cache-Control": "no-store", "Content-Type": "text/html; charset=utf-8" });
486
+ res.end(html(currentBuild.app.name ?? "Lakebed Capsule", { shooBaseUrl }));
418
487
  return;
419
488
  }
420
489
 
421
490
  if (requestUrl.pathname === "/client.js") {
422
- res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
423
- res.end(await readFile(built.clientOut, "utf8"));
491
+ res.writeHead(200, { "Cache-Control": "no-store", "Content-Type": "application/javascript; charset=utf-8" });
492
+ res.end(await readFile(currentBuild.clientOut, "utf8"));
424
493
  return;
425
494
  }
426
495
 
@@ -457,11 +526,11 @@ export async function startDevServer({
457
526
  for (const name of subscription.queries) {
458
527
  try {
459
528
  const data = await runQuery({
460
- app: built.app,
529
+ app: currentBuild.app,
461
530
  stateCell,
462
531
  auth: subscription.auth,
463
532
  logs,
464
- env: built.env,
533
+ env: currentBuild.env,
465
534
  name
466
535
  });
467
536
  sendJson(ws, { op: "query.result", name, data });
@@ -493,11 +562,11 @@ export async function startDevServer({
493
562
  if (message.op === "query.subscribe") {
494
563
  subscription.queries.add(message.name);
495
564
  const data = await runQuery({
496
- app: built.app,
565
+ app: currentBuild.app,
497
566
  stateCell,
498
567
  auth: subscription.auth,
499
568
  logs,
500
- env: built.env,
569
+ env: currentBuild.env,
501
570
  name: message.name
502
571
  });
503
572
  sendJson(ws, { id: message.id, op: "query.result", ok: true, name: message.name, data });
@@ -506,11 +575,11 @@ export async function startDevServer({
506
575
 
507
576
  if (message.op === "mutation.run") {
508
577
  const { result } = await runMutation({
509
- app: built.app,
578
+ app: currentBuild.app,
510
579
  stateCell,
511
580
  auth: subscription.auth,
512
581
  logs,
513
- env: built.env,
582
+ env: currentBuild.env,
514
583
  name: message.name,
515
584
  args: message.args ?? []
516
585
  });
@@ -570,15 +639,42 @@ export async function startDevServer({
570
639
  console.log(`Auth: ${defaultAuth.userId}`);
571
640
  }
572
641
 
642
+ const watchInterval = sourceStore
643
+ ? null
644
+ : setInterval(async () => {
645
+ try {
646
+ const nextFingerprint = await capsuleFileFingerprint(resolvedCapsuleDir);
647
+ if (nextFingerprint === fileFingerprint) {
648
+ return;
649
+ }
650
+
651
+ fileFingerprint = nextFingerprint;
652
+ scheduleRebuild();
653
+ } catch (error) {
654
+ logs.append("error", "dev server file watch failed", { error: error instanceof Error ? error.message : String(error) });
655
+ }
656
+ }, 300);
657
+
573
658
  return {
574
- app: built.app,
575
- buildDir: built.buildDir,
659
+ get app() {
660
+ return currentBuild.app;
661
+ },
662
+ get buildDir() {
663
+ return currentBuild.buildDir;
664
+ },
576
665
  capsuleDir: resolvedCapsuleDir,
577
666
  logs,
578
667
  port,
579
668
  stateCell,
580
669
  url: `http://localhost:${port}`,
581
670
  async close() {
671
+ if (watchInterval) {
672
+ clearInterval(watchInterval);
673
+ }
674
+ if (rebuildTimer) {
675
+ clearTimeout(rebuildTimer);
676
+ }
677
+ await rebuildPromise.catch(() => {});
582
678
  for (const client of wss.clients) {
583
679
  client.close();
584
680
  }
@@ -831,7 +927,7 @@ async function readResponseJson(response) {
831
927
 
832
928
  async function deployCommand(args) {
833
929
  const [capsuleArg] = positionals(args);
834
- const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
930
+ const capsuleDir = resolveCapsuleDir(capsuleArg);
835
931
  const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
836
932
  const serverEnvFileExists = sourceStore.hasFile(SERVER_ENV_FILE);
837
933
  const serverEnv = await readCapsuleServerEnv(sourceStore);
@@ -973,7 +1069,7 @@ async function deployCommand(args) {
973
1069
 
974
1070
  async function claimCommand(args) {
975
1071
  const [capsuleArg] = positionals(args);
976
- const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
1072
+ const capsuleDir = resolveCapsuleDir(capsuleArg);
977
1073
  const api = deployApiUrl(args);
978
1074
  const metadata = await readDeployMetadata(capsuleDir);
979
1075
 
@@ -1153,7 +1249,7 @@ This is a Lakebed capsule. Build the app inside this directory using the Lakebed
1153
1249
  Run locally:
1154
1250
 
1155
1251
  \`\`\`sh
1156
- lakebed dev .
1252
+ lakebed dev
1157
1253
  \`\`\`
1158
1254
 
1159
1255
  Deploy:
package/src/runtime.js CHANGED
@@ -187,6 +187,22 @@ export class StateCell {
187
187
  }
188
188
  }
189
189
 
190
+ updateSchema(schema) {
191
+ this.schema = schema;
192
+
193
+ for (const tableName of Object.keys(schema ?? {})) {
194
+ if (!this.tables.has(tableName)) {
195
+ this.tables.set(tableName, new Map());
196
+ }
197
+ }
198
+
199
+ for (const tableName of this.tables.keys()) {
200
+ if (!schema?.[tableName]) {
201
+ this.tables.delete(tableName);
202
+ }
203
+ }
204
+ }
205
+
190
206
  createDb() {
191
207
  const db = {};
192
208
  for (const tableName of this.tables.keys()) {
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export const LAKEBED_VERSION = "0.0.10";
1
+ export const LAKEBED_VERSION = "0.0.12";