lakebed 0.0.1 → 0.0.3-alpha.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/README.md +5 -3
- package/package.json +1 -1
- package/src/anonymous-server.js +139 -12
- package/src/anonymous.js +1 -1
- package/src/cli.js +155 -15
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ export default capsule({
|
|
|
61
61
|
lakebed new <name> [--template todo]
|
|
62
62
|
lakebed dev <capsule-dir> [--port 3000]
|
|
63
63
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
64
|
-
lakebed deploy
|
|
64
|
+
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
65
65
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>]
|
|
66
66
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
67
67
|
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
@@ -83,12 +83,14 @@ lakebed logs [deploy-id-or-url] [--port 3000]
|
|
|
83
83
|
Once a Lakebed deploy runner is available, deploy a capsule with:
|
|
84
84
|
|
|
85
85
|
```sh
|
|
86
|
-
|
|
86
|
+
cd my-app
|
|
87
|
+
lakebed deploy
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
For local deploy-runner testing:
|
|
90
91
|
|
|
91
92
|
```sh
|
|
92
93
|
lakebed anonymous-server --port 8787
|
|
93
|
-
|
|
94
|
+
cd my-app
|
|
95
|
+
lakebed deploy --api http://localhost:8787
|
|
94
96
|
```
|
package/package.json
CHANGED
package/src/anonymous-server.js
CHANGED
|
@@ -108,6 +108,20 @@ async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
|
108
108
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function bearerToken(req) {
|
|
112
|
+
const header = req.headers.authorization;
|
|
113
|
+
if (!header) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const match = String(header).match(/^Bearer\s+(.+)$/i);
|
|
118
|
+
return match?.[1]?.trim() ?? "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isDeployTokenValid(deploy, token) {
|
|
122
|
+
return Boolean(token && deploy?.claimTokenHash && hashClaimToken(token) === deploy.claimTokenHash);
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
function normalizePublicRootUrl(value, port) {
|
|
112
126
|
const fallback = `http://localhost:${port}`;
|
|
113
127
|
return String(value || fallback).replace(/\/+$/g, "");
|
|
@@ -220,6 +234,19 @@ export class MemoryAnonymousStore {
|
|
|
220
234
|
|
|
221
235
|
async initialize() {}
|
|
222
236
|
|
|
237
|
+
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
238
|
+
const currentArtifact = this.artifacts.get(artifactHash);
|
|
239
|
+
this.artifacts.set(artifactHash, {
|
|
240
|
+
artifact,
|
|
241
|
+
bytes: Buffer.byteLength(clientBundleBase64, "base64"),
|
|
242
|
+
clientBundleBase64,
|
|
243
|
+
clientBundleHash,
|
|
244
|
+
createdAt,
|
|
245
|
+
hash: artifactHash,
|
|
246
|
+
refCount: (currentArtifact?.refCount ?? 0) + 1
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
224
251
|
const deployId = createDeployId();
|
|
225
252
|
let slug = createSlug();
|
|
@@ -233,15 +260,12 @@ export class MemoryAnonymousStore {
|
|
|
233
260
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
234
261
|
const url = appUrlForSlug({ appBaseDomain, publicRootUrl, slug });
|
|
235
262
|
|
|
236
|
-
|
|
237
|
-
this.artifacts.set(artifactHash, {
|
|
263
|
+
this.storeArtifact({
|
|
238
264
|
artifact,
|
|
239
|
-
bytes: Buffer.byteLength(clientBundleBase64, "base64"),
|
|
240
265
|
clientBundleBase64,
|
|
241
266
|
clientBundleHash,
|
|
242
267
|
createdAt,
|
|
243
|
-
|
|
244
|
-
refCount: (currentArtifact?.refCount ?? 0) + 1
|
|
268
|
+
artifactHash
|
|
245
269
|
});
|
|
246
270
|
|
|
247
271
|
const deploy = {
|
|
@@ -263,6 +287,33 @@ export class MemoryAnonymousStore {
|
|
|
263
287
|
return { deploy, token };
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
async updateDeploy({ artifact, artifactHash, clientBundleBase64, clientBundleHash, deployId, requestedTtlSeconds }) {
|
|
291
|
+
const currentDeploy = await this.getDeployById(deployId);
|
|
292
|
+
if (!currentDeploy) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const createdAt = now();
|
|
297
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
298
|
+
const deploy = {
|
|
299
|
+
...currentDeploy,
|
|
300
|
+
artifactHash,
|
|
301
|
+
clientBundleHash,
|
|
302
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
|
303
|
+
status: "active"
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
this.storeArtifact({
|
|
307
|
+
artifact,
|
|
308
|
+
artifactHash,
|
|
309
|
+
clientBundleBase64,
|
|
310
|
+
clientBundleHash,
|
|
311
|
+
createdAt
|
|
312
|
+
});
|
|
313
|
+
this.deploys.set(deployId, deploy);
|
|
314
|
+
return deploy;
|
|
315
|
+
}
|
|
316
|
+
|
|
266
317
|
async getDeployById(id) {
|
|
267
318
|
return this.deploys.get(id) ?? null;
|
|
268
319
|
}
|
|
@@ -467,13 +518,7 @@ export class PostgresAnonymousStore {
|
|
|
467
518
|
return this.pool.query(sql, params);
|
|
468
519
|
}
|
|
469
520
|
|
|
470
|
-
async
|
|
471
|
-
const createdAt = now();
|
|
472
|
-
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
473
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
474
|
-
const token = createClaimToken();
|
|
475
|
-
const deployId = createDeployId();
|
|
476
|
-
|
|
521
|
+
async storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
477
522
|
await this.query(
|
|
478
523
|
`
|
|
479
524
|
insert into artifacts(hash, artifact_json, client_bundle_base64, client_bundle_hash, bytes, created_at, ref_count)
|
|
@@ -482,6 +527,16 @@ export class PostgresAnonymousStore {
|
|
|
482
527
|
`,
|
|
483
528
|
[artifactHash, JSON.stringify(artifact), clientBundleBase64, clientBundleHash, Buffer.byteLength(clientBundleBase64, "base64"), createdAt]
|
|
484
529
|
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async createDeploy({ appBaseDomain, artifact, artifactHash, clientBundleBase64, clientBundleHash, publicRootUrl, requestedTtlSeconds }) {
|
|
533
|
+
const createdAt = now();
|
|
534
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
535
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
536
|
+
const token = createClaimToken();
|
|
537
|
+
const deployId = createDeployId();
|
|
538
|
+
|
|
539
|
+
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt });
|
|
485
540
|
|
|
486
541
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
487
542
|
const slug = createSlug();
|
|
@@ -536,6 +591,29 @@ export class PostgresAnonymousStore {
|
|
|
536
591
|
throw new Error("Unable to allocate anonymous deploy slug.");
|
|
537
592
|
}
|
|
538
593
|
|
|
594
|
+
async updateDeploy({ artifact, artifactHash, clientBundleBase64, clientBundleHash, deployId, requestedTtlSeconds }) {
|
|
595
|
+
const currentDeploy = await this.getDeployById(deployId);
|
|
596
|
+
if (!currentDeploy) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const createdAt = now();
|
|
601
|
+
const ttlSeconds = parseTtlSeconds(requestedTtlSeconds);
|
|
602
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
603
|
+
await this.storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt });
|
|
604
|
+
|
|
605
|
+
const result = await this.query(
|
|
606
|
+
`
|
|
607
|
+
update deploys
|
|
608
|
+
set status = 'active', artifact_hash = $2, client_bundle_hash = $3, expires_at = $4
|
|
609
|
+
where id = $1
|
|
610
|
+
returning *
|
|
611
|
+
`,
|
|
612
|
+
[deployId, artifactHash, clientBundleHash, expiresAt]
|
|
613
|
+
);
|
|
614
|
+
return this.rowToDeploy(result.rows[0]);
|
|
615
|
+
}
|
|
616
|
+
|
|
539
617
|
rowToDeploy(row) {
|
|
540
618
|
if (!row) {
|
|
541
619
|
return null;
|
|
@@ -834,6 +912,20 @@ export async function startAnonymousServer({
|
|
|
834
912
|
await resolvedStore.initialize();
|
|
835
913
|
const subscriptions = new Map();
|
|
836
914
|
|
|
915
|
+
async function refreshDeploySubscriptions(deploy) {
|
|
916
|
+
const storedArtifact = await resolvedStore.getArtifact(deploy.artifactHash);
|
|
917
|
+
if (!storedArtifact) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const subscription of subscriptions.values()) {
|
|
922
|
+
if (subscription.deploy.id === deploy.id) {
|
|
923
|
+
subscription.artifact = storedArtifact.artifact;
|
|
924
|
+
subscription.deploy = deploy;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
837
929
|
async function publishDeploy(deployId) {
|
|
838
930
|
for (const [ws, subscription] of subscriptions) {
|
|
839
931
|
if (subscription.deploy.id !== deployId) {
|
|
@@ -884,6 +976,41 @@ export async function startAnonymousServer({
|
|
|
884
976
|
return;
|
|
885
977
|
}
|
|
886
978
|
|
|
979
|
+
if ((req.method === "PUT" || req.method === "PATCH") && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
980
|
+
const deployId = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
981
|
+
const currentDeploy = await resolvedStore.getDeployById(deployId);
|
|
982
|
+
if (!currentDeploy) {
|
|
983
|
+
sendJson(res, 404, { error: "Unknown deploy." });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!isDeployTokenValid(currentDeploy, bearerToken(req))) {
|
|
988
|
+
sendJson(res, 401, { error: "Invalid deploy token." });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const body = await readJsonBody(req);
|
|
993
|
+
const payload = validateAnonymousDeployPayload(body);
|
|
994
|
+
const deploy = await resolvedStore.updateDeploy({
|
|
995
|
+
artifact: payload.artifact,
|
|
996
|
+
artifactHash: payload.artifactHash,
|
|
997
|
+
clientBundleBase64: payload.clientBundleBase64,
|
|
998
|
+
clientBundleHash: payload.clientBundleHash,
|
|
999
|
+
deployId,
|
|
1000
|
+
requestedTtlSeconds: body.requestedTtlSeconds ?? requestUrl.searchParams.get("ttl") ?? undefined
|
|
1001
|
+
});
|
|
1002
|
+
if (!deploy) {
|
|
1003
|
+
sendJson(res, 404, { error: "Unknown deploy." });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
await resolvedStore.appendLog(deploy.id, "info", "anonymous deploy updated", { artifactHash: deploy.artifactHash });
|
|
1008
|
+
await refreshDeploySubscriptions(deploy);
|
|
1009
|
+
await publishDeploy(deploy.id);
|
|
1010
|
+
sendJson(res, 200, responseForDeploy({ deploy }));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
887
1014
|
if (req.method === "GET" && requestUrl.pathname.startsWith("/v1/deploys/")) {
|
|
888
1015
|
const id = requestUrl.pathname.slice("/v1/deploys/".length);
|
|
889
1016
|
const deploy = (await resolvedStore.getDeployById(id)) ?? (await resolvedStore.getDeployBySlug(id));
|
package/src/anonymous.js
CHANGED
|
@@ -484,7 +484,7 @@ function compileServerToIr(app, schema) {
|
|
|
484
484
|
return { diagnostics, mutations, queries };
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = "0.0.
|
|
487
|
+
export async function createAnonymousArtifact({ app, clientOut, sourceStore, version = "0.0.3-alpha.0" }) {
|
|
488
488
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
489
489
|
const diagnostics = forbiddenSourceDiagnostics(sourceFiles);
|
|
490
490
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
package/src/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const root = process.cwd();
|
|
|
21
21
|
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
22
|
const packageNodeModules = resolve(packageDir, "node_modules");
|
|
23
23
|
const sourceNamespace = "lakebed-source";
|
|
24
|
+
const defaultDeployApiUrl = "https://api.lakebed.app";
|
|
24
25
|
|
|
25
26
|
function usage() {
|
|
26
27
|
console.log(`lakebed
|
|
@@ -29,7 +30,7 @@ Usage:
|
|
|
29
30
|
lakebed new <name> [--template todo]
|
|
30
31
|
lakebed dev <capsule-dir> [--port 3000]
|
|
31
32
|
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
32
|
-
lakebed deploy
|
|
33
|
+
lakebed deploy [capsule-dir] [--ttl 7d] [--api <url>] [--json]
|
|
33
34
|
lakebed anonymous-server [--port 8787] [--public-root-url <url>]
|
|
34
35
|
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
35
36
|
lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
|
|
@@ -614,6 +615,55 @@ function defaultArtifactPath(capsuleDir) {
|
|
|
614
615
|
return resolve(root, ".lakebed/artifacts", `${basename(capsuleDir)}.anonymous.json`);
|
|
615
616
|
}
|
|
616
617
|
|
|
618
|
+
function deployMetadataPath(capsuleDir) {
|
|
619
|
+
return resolve(capsuleDir, ".lakebed/deploy.json");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function readDeployMetadata(capsuleDir) {
|
|
623
|
+
try {
|
|
624
|
+
return JSON.parse(await readFile(deployMetadataPath(capsuleDir), "utf8"));
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error?.code === "ENOENT") {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
throw new Error(`Unable to read Lakebed deploy metadata: ${error instanceof Error ? error.message : String(error)}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function writeDeployMetadata(capsuleDir, metadata) {
|
|
635
|
+
const path = deployMetadataPath(capsuleDir);
|
|
636
|
+
await mkdir(dirname(path), { recursive: true });
|
|
637
|
+
await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function claimTokenFromDeployResponse(deployed) {
|
|
641
|
+
if (!deployed?.claimUrl || !deployed?.deployId) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const url = new URL(deployed.claimUrl);
|
|
647
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
648
|
+
if (segments[0] === "claim" && segments[1] === deployed.deployId) {
|
|
649
|
+
return segments[2] ?? null;
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function deployRequestBody(envelope, ttl) {
|
|
659
|
+
return JSON.stringify({
|
|
660
|
+
artifact: envelope.artifact,
|
|
661
|
+
clientBundle: envelope.clientBundle,
|
|
662
|
+
clientVersion: "0.0.3-alpha.0",
|
|
663
|
+
requestedTtlSeconds: ttl
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
617
667
|
async function buildCommand(args) {
|
|
618
668
|
const [capsuleArg] = positionals(args);
|
|
619
669
|
const target = readArg(args, "--target", "anonymous");
|
|
@@ -649,10 +699,7 @@ async function buildCommand(args) {
|
|
|
649
699
|
}
|
|
650
700
|
|
|
651
701
|
function deployApiUrl(args) {
|
|
652
|
-
return String(readArg(args, "--api", process.env.LAKEBED_DEPLOY_API ?? process.env.SPAN_DEPLOY_API ??
|
|
653
|
-
/\/+$/g,
|
|
654
|
-
""
|
|
655
|
-
);
|
|
702
|
+
return String(readArg(args, "--api", process.env.LAKEBED_DEPLOY_API ?? process.env.SPAN_DEPLOY_API ?? defaultDeployApiUrl)).replace(/\/+$/g, "");
|
|
656
703
|
}
|
|
657
704
|
|
|
658
705
|
async function readResponseJson(response) {
|
|
@@ -665,32 +712,65 @@ async function readResponseJson(response) {
|
|
|
665
712
|
|
|
666
713
|
async function deployCommand(args) {
|
|
667
714
|
const [capsuleArg] = positionals(args);
|
|
668
|
-
const
|
|
715
|
+
const capsuleDir = capsuleArg ? resolveCapsuleDir(capsuleArg) : root;
|
|
716
|
+
const envelope = await buildAnonymousEnvelope(capsuleDir);
|
|
669
717
|
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
670
718
|
const api = deployApiUrl(args);
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
719
|
+
const body = deployRequestBody(envelope, ttl);
|
|
720
|
+
const metadata = await readDeployMetadata(capsuleDir);
|
|
721
|
+
const canUpdate =
|
|
722
|
+
metadata?.api === api && typeof metadata?.deployId === "string" && typeof metadata?.claimToken === "string";
|
|
723
|
+
let mode = "created";
|
|
724
|
+
let response;
|
|
725
|
+
|
|
726
|
+
if (canUpdate) {
|
|
727
|
+
response = await fetch(`${api}/v1/deploys/${encodeURIComponent(metadata.deployId)}`, {
|
|
728
|
+
body,
|
|
729
|
+
headers: {
|
|
730
|
+
"Authorization": `Bearer ${metadata.claimToken}`,
|
|
731
|
+
"Content-Type": "application/json"
|
|
732
|
+
},
|
|
733
|
+
method: "PUT"
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (response.status === 404 || response.status === 410) {
|
|
737
|
+
mode = "created";
|
|
738
|
+
response = null;
|
|
739
|
+
} else {
|
|
740
|
+
mode = "updated";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
response ??= await fetch(`${api}/v1/anonymous-deploys`, {
|
|
745
|
+
body,
|
|
678
746
|
headers: {
|
|
679
747
|
"Content-Type": "application/json"
|
|
680
748
|
},
|
|
681
749
|
method: "POST"
|
|
682
750
|
});
|
|
683
751
|
const deployed = await readResponseJson(response);
|
|
752
|
+
const claimToken = claimTokenFromDeployResponse(deployed) ?? metadata?.claimToken;
|
|
753
|
+
if (claimToken) {
|
|
754
|
+
await writeDeployMetadata(capsuleDir, {
|
|
755
|
+
api,
|
|
756
|
+
claimToken,
|
|
757
|
+
deployId: deployed.deployId,
|
|
758
|
+
updatedAt: new Date().toISOString(),
|
|
759
|
+
url: deployed.url
|
|
760
|
+
});
|
|
761
|
+
}
|
|
684
762
|
|
|
685
763
|
if (hasFlag(args, "--json")) {
|
|
686
764
|
console.log(JSON.stringify(deployed, null, 2));
|
|
687
765
|
return;
|
|
688
766
|
}
|
|
689
767
|
|
|
690
|
-
console.log("
|
|
768
|
+
console.log(`${mode === "updated" ? "Updated" : "Created"} anonymous preview.\n`);
|
|
691
769
|
console.log(`App: ${deployed.url}`);
|
|
692
770
|
console.log(`Expires: ${deployed.expiresAt}`);
|
|
693
|
-
|
|
771
|
+
if (deployed.claimUrl) {
|
|
772
|
+
console.log(`Claim: ${deployed.claimUrl}`);
|
|
773
|
+
}
|
|
694
774
|
console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
|
|
695
775
|
console.log("\nLimits:");
|
|
696
776
|
console.log(` source/artifact: ${deployed.limits.artifactBytes} bytes`);
|
|
@@ -813,9 +893,67 @@ async function dbCommand(args) {
|
|
|
813
893
|
usage();
|
|
814
894
|
}
|
|
815
895
|
|
|
896
|
+
function agentInstructionsTemplate() {
|
|
897
|
+
return `# Lakebed App Instructions
|
|
898
|
+
|
|
899
|
+
This is a Lakebed capsule. Build the app inside this directory using the Lakebed v0 contract.
|
|
900
|
+
|
|
901
|
+
## File Layout
|
|
902
|
+
|
|
903
|
+
- \`server/index.ts\`: schema, queries, and mutations.
|
|
904
|
+
- \`client/index.tsx\`: Preact UI entrypoint.
|
|
905
|
+
- \`shared/\`: pure TypeScript shared by client and server.
|
|
906
|
+
|
|
907
|
+
## Commands
|
|
908
|
+
|
|
909
|
+
Run locally:
|
|
910
|
+
|
|
911
|
+
\`\`\`sh
|
|
912
|
+
lakebed dev .
|
|
913
|
+
\`\`\`
|
|
914
|
+
|
|
915
|
+
Deploy:
|
|
916
|
+
|
|
917
|
+
\`\`\`sh
|
|
918
|
+
lakebed deploy
|
|
919
|
+
\`\`\`
|
|
920
|
+
|
|
921
|
+
Inspect local state while \`lakebed dev\` is running:
|
|
922
|
+
|
|
923
|
+
\`\`\`sh
|
|
924
|
+
lakebed db list --port 3000
|
|
925
|
+
lakebed db dump --port 3000
|
|
926
|
+
lakebed logs --port 3000
|
|
927
|
+
\`\`\`
|
|
928
|
+
|
|
929
|
+
## Rules
|
|
930
|
+
|
|
931
|
+
- Use \`lakebed/server\` only from \`server/index.ts\`.
|
|
932
|
+
- Use \`lakebed/client\` only from \`client/index.tsx\`.
|
|
933
|
+
- Do not import npm packages from app code.
|
|
934
|
+
- Do not use Node built-ins in app code.
|
|
935
|
+
- Use Tailwind classes directly in JSX.
|
|
936
|
+
- Do not add a CSS, PostCSS, or Tailwind build pipeline.
|
|
937
|
+
- Use guest auth through \`ctx.auth\` on the server and \`useAuth()\` on the client.
|
|
938
|
+
- Keep \`shared/\` free of DOM, Node, env, and Lakebed runtime imports.
|
|
939
|
+
|
|
940
|
+
## Current Limits
|
|
941
|
+
|
|
942
|
+
- One server entry.
|
|
943
|
+
- One client entry.
|
|
944
|
+
- Guest auth only.
|
|
945
|
+
- No file storage.
|
|
946
|
+
- No outbound fetch in anonymous deploys.
|
|
947
|
+
- Local state resets when \`lakebed dev\` restarts.
|
|
948
|
+
`;
|
|
949
|
+
}
|
|
950
|
+
|
|
816
951
|
function todoTemplate(name) {
|
|
817
952
|
const title = basename(name);
|
|
953
|
+
const agentInstructions = agentInstructionsTemplate();
|
|
818
954
|
return {
|
|
955
|
+
"AGENTS.md": agentInstructions,
|
|
956
|
+
"CLAUDE.md": agentInstructions,
|
|
819
957
|
"server/index.ts": `import { boolean, capsule, mutation, query, string, table } from "lakebed/server";
|
|
820
958
|
import { cleanTodoText } from "../shared/todo";
|
|
821
959
|
|
|
@@ -901,6 +1039,8 @@ export function App() {
|
|
|
901
1039
|
export function cleanTodoText(value: string): string {
|
|
902
1040
|
return value.trim().slice(0, 160);
|
|
903
1041
|
}
|
|
1042
|
+
`,
|
|
1043
|
+
".gitignore": `.lakebed/
|
|
904
1044
|
`,
|
|
905
1045
|
"README.md": `# ${title}
|
|
906
1046
|
|