bunderstack 0.17.0-beta.8 → 0.17.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/dist/cli-skills.d.ts +16 -0
- package/dist/cli-skills.d.ts.map +1 -0
- package/dist/cli-skills.js +116 -0
- package/dist/cli-skills.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +36 -4
- package/dist/cli.js.map +1 -1
- package/llms.txt +360 -0
- package/package.json +3 -1
- package/skills/creating-bunderstack-apps/SKILL.md +46 -0
- package/skills/creating-bunderstack-apps/agents/openai.yaml +4 -0
- package/skills/creating-bunderstack-apps/references/application-structure.md +197 -0
- package/skills/creating-bunderstack-apps/references/runtime-integrations.md +68 -0
- package/skills/creating-bunderstack-apps/references/verification.md +25 -0
- package/skills/migrating-to-bunderstack/SKILL.md +84 -0
- package/skills/migrating-to-bunderstack/agents/openai.yaml +4 -0
- package/skills/migrating-to-bunderstack/references/audit-checklist.md +57 -0
- package/skills/migrating-to-bunderstack/references/runtime-replacements.md +286 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The skills this package ships, copied from `.agents/skills` at build time. */
|
|
2
|
+
export declare const SHIPPED_SKILLS: readonly ["creating-bunderstack-apps", "migrating-to-bunderstack"];
|
|
3
|
+
export type SkillsOptions = {
|
|
4
|
+
/** Project root the skills are installed into. */
|
|
5
|
+
cwd: string;
|
|
6
|
+
/** Destination, relative to `cwd`. */
|
|
7
|
+
directory?: string;
|
|
8
|
+
/** Report drift instead of writing. Exits non-zero when anything differs. */
|
|
9
|
+
check?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type SkillsIo = {
|
|
12
|
+
stdout(message: string): void;
|
|
13
|
+
stderr(message: string): void;
|
|
14
|
+
};
|
|
15
|
+
export declare function installSkills(options: SkillsOptions, io: SkillsIo): Promise<number>;
|
|
16
|
+
//# sourceMappingURL=cli-skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-skills.d.ts","sourceRoot":"","sources":["../src/cli-skills.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,eAAO,MAAM,cAAc,oEAGjB,CAAA;AAwBV,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAA;IACX,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAuDD,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,QAAQ,GACX,OAAO,CAAC,MAAM,CAAC,CAiDjB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** The skills this package ships, copied from `.agents/skills` at build time. */
|
|
5
|
+
export const SHIPPED_SKILLS = [
|
|
6
|
+
'creating-bunderstack-apps',
|
|
7
|
+
'migrating-to-bunderstack',
|
|
8
|
+
];
|
|
9
|
+
const MARKER_START = '<!-- bunderstack:skills -->';
|
|
10
|
+
const MARKER_END = '<!-- /bunderstack:skills -->';
|
|
11
|
+
/**
|
|
12
|
+
* Agents read AGENTS.md before they search for anything, so the pointer is
|
|
13
|
+
* what actually gets the skills into context. The markers let a later
|
|
14
|
+
* `bunderstack skills` run replace the block without touching the rest.
|
|
15
|
+
*/
|
|
16
|
+
function agentsBlock(directory) {
|
|
17
|
+
return `${MARKER_START}
|
|
18
|
+
## Bunderstack
|
|
19
|
+
|
|
20
|
+
This project uses Bunderstack. Before changing the server API, access rules,
|
|
21
|
+
jobs, storage, or realtime, read \`${directory}/creating-bunderstack-apps/SKILL.md\`.
|
|
22
|
+
When replacing existing infrastructure, read
|
|
23
|
+
\`${directory}/migrating-to-bunderstack/SKILL.md\` instead.
|
|
24
|
+
|
|
25
|
+
\`node_modules/bunderstack/llms.txt\` is a dense plain-text reference for the
|
|
26
|
+
whole framework.
|
|
27
|
+
${MARKER_END}`;
|
|
28
|
+
}
|
|
29
|
+
/** Where this build keeps its copy of the skills. */
|
|
30
|
+
function packagedSkillsDir() {
|
|
31
|
+
return join(new URL('..', import.meta.url).pathname, 'skills');
|
|
32
|
+
}
|
|
33
|
+
async function filesUnder(dir) {
|
|
34
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
35
|
+
return entries
|
|
36
|
+
.filter((entry) => entry.isFile())
|
|
37
|
+
.map((entry) => join(entry.parentPath, entry.name));
|
|
38
|
+
}
|
|
39
|
+
async function differs(from, to) {
|
|
40
|
+
if (!existsSync(to))
|
|
41
|
+
return true;
|
|
42
|
+
const sources = await filesUnder(from);
|
|
43
|
+
for (const source of sources) {
|
|
44
|
+
const target = join(to, source.slice(from.length + 1));
|
|
45
|
+
if (!existsSync(target))
|
|
46
|
+
return true;
|
|
47
|
+
if ((await readFile(source, 'utf8')) !== (await readFile(target, 'utf8'))) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
async function writeAgentsPointer(cwd, directory) {
|
|
54
|
+
const path = join(cwd, 'AGENTS.md');
|
|
55
|
+
const block = agentsBlock(directory);
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
await writeFile(path, `# Agent guide\n\n${block}\n`);
|
|
58
|
+
return 'created';
|
|
59
|
+
}
|
|
60
|
+
const current = await readFile(path, 'utf8');
|
|
61
|
+
const start = current.indexOf(MARKER_START);
|
|
62
|
+
const end = current.indexOf(MARKER_END);
|
|
63
|
+
if (start !== -1 && end !== -1) {
|
|
64
|
+
const replaced = current.slice(0, start) + block + current.slice(end + MARKER_END.length);
|
|
65
|
+
if (replaced === current)
|
|
66
|
+
return 'current';
|
|
67
|
+
await writeFile(path, replaced);
|
|
68
|
+
return 'updated';
|
|
69
|
+
}
|
|
70
|
+
await writeFile(path, `${current.trimEnd()}\n\n${block}\n`);
|
|
71
|
+
return 'updated';
|
|
72
|
+
}
|
|
73
|
+
export async function installSkills(options, io) {
|
|
74
|
+
const source = packagedSkillsDir();
|
|
75
|
+
if (!existsSync(source)) {
|
|
76
|
+
io.stderr('[bunderstack] this build ships no skills; reinstall the package or run bun run build');
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const directory = options.directory ?? '.agents/skills';
|
|
80
|
+
const destination = join(options.cwd, directory);
|
|
81
|
+
const stale = [];
|
|
82
|
+
for (const skill of SHIPPED_SKILLS) {
|
|
83
|
+
const from = join(source, skill);
|
|
84
|
+
const to = join(destination, skill);
|
|
85
|
+
if (!(await differs(from, to)))
|
|
86
|
+
continue;
|
|
87
|
+
stale.push(skill);
|
|
88
|
+
if (options.check)
|
|
89
|
+
continue;
|
|
90
|
+
await mkdir(destination, { recursive: true });
|
|
91
|
+
await cp(from, to, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
if (options.check) {
|
|
94
|
+
const pointer = await readFile(join(options.cwd, 'AGENTS.md'), 'utf8').catch(() => '');
|
|
95
|
+
const pointerStale = !pointer.includes(MARKER_START);
|
|
96
|
+
if (stale.length === 0 && !pointerStale) {
|
|
97
|
+
io.stdout(`${directory} is current`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
for (const skill of stale) {
|
|
101
|
+
io.stderr(`[bunderstack] ${directory}/${skill} is missing or outdated`);
|
|
102
|
+
}
|
|
103
|
+
if (pointerStale)
|
|
104
|
+
io.stderr('[bunderstack] AGENTS.md has no Bunderstack block');
|
|
105
|
+
io.stderr('[bunderstack] run: bunx bunderstack skills');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const pointer = await writeAgentsPointer(options.cwd, directory);
|
|
109
|
+
io.stdout(stale.length === 0
|
|
110
|
+
? `${directory} is current`
|
|
111
|
+
: `Installed ${stale.length} skill(s) into ${directory}: ${stale.join(', ')}`);
|
|
112
|
+
if (pointer !== 'current')
|
|
113
|
+
io.stdout(`AGENTS.md ${pointer}`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=cli-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-skills.js","sourceRoot":"","sources":["../src/cli-skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,iFAAiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,2BAA2B;IAC3B,0BAA0B;CAClB,CAAA;AAEV,MAAM,YAAY,GAAG,6BAA6B,CAAA;AAClD,MAAM,UAAU,GAAG,8BAA8B,CAAA;AAEjD;;;;GAIG;AACH,SAAS,WAAW,CAAC,SAAiB;IACpC,OAAO,GAAG,YAAY;;;;qCAIa,SAAS;;IAE1C,SAAS;;;;EAIX,UAAU,EAAE,CAAA;AACd,CAAC;AAgBD,qDAAqD;AACrD,SAAS,iBAAiB;IACxB,OAAO,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAChE,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,GAAW;IACnC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5E,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;AACvD,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAY,EAAE,EAAU;IAC7C,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAA;IAChC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAA;IACtC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;QACtD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAA;QACpC,IAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;YAC1E,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,KAAK,UAAU,kBAAkB,CAC/B,GAAW,EACX,SAAiB;IAEjB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;IACnC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAA;IAEpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,SAAS,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC,CAAA;QACpD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAEvC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QAC/B,MAAM,QAAQ,GACZ,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;QAC1E,IAAI,QAAQ,KAAK,OAAO;YAAE,OAAO,SAAS,CAAA;QAC1C,MAAM,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC/B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,SAAS,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC,CAAA;IAC3D,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAsB,EACtB,EAAY;IAEZ,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,MAAM,CACP,sFAAsF,CACvF,CAAA;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAA;IACvD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;IAChD,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;QACnC,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAAE,SAAQ;QACxC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjB,IAAI,OAAO,CAAC,KAAK;YAAE,SAAQ;QAC3B,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7C,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACzC,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAC1E,GAAG,EAAE,CAAC,EAAE,CACT,CAAA;QACD,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAA;QACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,EAAE,CAAC,MAAM,CAAC,GAAG,SAAS,aAAa,CAAC,CAAA;YACpC,OAAO,CAAC,CAAA;QACV,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;YAC1B,EAAE,CAAC,MAAM,CAAC,iBAAiB,SAAS,IAAI,KAAK,yBAAyB,CAAC,CAAA;QACzE,CAAC;QACD,IAAI,YAAY;YAAE,EAAE,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAA;QAC/E,EAAE,CAAC,MAAM,CAAC,4CAA4C,CAAC,CAAA;QACvD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAA;IAEhE,EAAE,CAAC,MAAM,CACP,KAAK,CAAC,MAAM,KAAK,CAAC;QAChB,CAAC,CAAC,GAAG,SAAS,aAAa;QAC3B,CAAC,CAAC,aAAa,KAAK,CAAC,MAAM,kBAAkB,SAAS,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChF,CAAA;IACD,IAAI,OAAO,KAAK,SAAS;QAAE,EAAE,CAAC,MAAM,CAAC,aAAa,OAAO,EAAE,CAAC,CAAA;IAC5D,OAAO,CAAC,CAAA;AACV,CAAC"}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAEL,iBAAiB,EAElB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAEL,iBAAiB,EAElB,MAAM,uBAAuB,CAAA;AAG9B,MAAM,MAAM,KAAK,GAAG;IAClB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAcD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,EAAE,EAAE,KAAK,EACT,QAAQ,GAAE,OAAO,iBAAqC,GACrD,OAAO,CAAC,MAAM,CAAC,CAmFjB"}
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { BlueprintCheckError, generateBlueprint, } from './blueprint-generator.js';
|
|
3
|
-
|
|
3
|
+
import { installSkills } from './cli-skills.js';
|
|
4
|
+
const help = `Usage:
|
|
5
|
+
bunderstack blueprint [directory] [--entry <path>] [--output <path>] [--check]
|
|
6
|
+
bunderstack skills [--dir <path>] [--check]
|
|
4
7
|
|
|
5
|
-
Generate a committed deployment declaration for a TanStack Start
|
|
6
|
-
Entry precedence: --entry,
|
|
8
|
+
blueprint Generate a committed deployment declaration for a TanStack Start
|
|
9
|
+
application. Entry precedence: --entry,
|
|
10
|
+
package.json#bunderstack.entry, src/bunderstack.ts.
|
|
11
|
+
|
|
12
|
+
skills Install the Bunderstack agent skills that match this version into
|
|
13
|
+
.agents/skills, and point AGENTS.md at them so an agent loads them
|
|
14
|
+
before touching the API. --check reports drift without writing.`;
|
|
7
15
|
export async function runCli(args, io, generate = generateBlueprint) {
|
|
8
16
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
9
17
|
io.stdout(help);
|
|
@@ -13,8 +21,32 @@ export async function runCli(args, io, generate = generateBlueprint) {
|
|
|
13
21
|
io.stdout((await Bun.file(new URL('../package.json', import.meta.url)).json()).version);
|
|
14
22
|
return 0;
|
|
15
23
|
}
|
|
24
|
+
if (args[0] === 'skills') {
|
|
25
|
+
const options = {
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
};
|
|
28
|
+
for (let index = 1; index < args.length; index++) {
|
|
29
|
+
const argument = args[index];
|
|
30
|
+
if (argument === '--check') {
|
|
31
|
+
options.check = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (argument === '--dir') {
|
|
35
|
+
const value = args[++index];
|
|
36
|
+
if (!value || value.startsWith('--')) {
|
|
37
|
+
io.stderr('[bunderstack] missing value for --dir');
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
options.directory = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
io.stderr(`[bunderstack] unknown option: ${argument}`);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
return installSkills(options, io);
|
|
47
|
+
}
|
|
16
48
|
if (args[0] !== 'blueprint') {
|
|
17
|
-
io.stderr(
|
|
49
|
+
io.stderr(help);
|
|
18
50
|
return 2;
|
|
19
51
|
}
|
|
20
52
|
const options = { directory: process.cwd() };
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,GAElB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
BUNDERSTACK
|
|
2
|
+
|
|
3
|
+
A batteries-included backend framework for Bun. Version 0.17 (beta).
|
|
4
|
+
Docs: https://bunderstack.dev/docs
|
|
5
|
+
This file is written for coding agents. It is dense on purpose.
|
|
6
|
+
|
|
7
|
+
WHAT IT IS
|
|
8
|
+
|
|
9
|
+
createBunderstack() takes a Drizzle schema and returns an app whose handler is
|
|
10
|
+
a single Web-Standard Request -> Response function. From the schema it
|
|
11
|
+
generates secured CRUD procedures. Your own procedures, file storage, realtime
|
|
12
|
+
subscriptions, and a health check live in the same oRPC graph, reachable both
|
|
13
|
+
as typed RPC and as ordinary HTTP.
|
|
14
|
+
|
|
15
|
+
Stack: Bun, Drizzle (+ drizzle-kit), Better Auth, oRPC v2, libSQL or Postgres,
|
|
16
|
+
sharp. Validation accepts any Standard Schema library; generated schemas use
|
|
17
|
+
Valibot. There is no Hono, no tRPC, and no Zod requirement.
|
|
18
|
+
|
|
19
|
+
Packages: bunderstack (server), bunderstack-query (client + TanStack Query),
|
|
20
|
+
bunderstack-sync (TanStack DB collections), bunderstack-start (TanStack Start
|
|
21
|
+
integration).
|
|
22
|
+
|
|
23
|
+
MINIMAL APP
|
|
24
|
+
|
|
25
|
+
import { createBunderstack } from 'bunderstack'
|
|
26
|
+
import { libsql } from 'bunderstack/database/libsql'
|
|
27
|
+
import * as schema from './schema'
|
|
28
|
+
|
|
29
|
+
export const app = await createBunderstack({
|
|
30
|
+
schema,
|
|
31
|
+
database: { adapter: libsql(), url: 'file:./data.db' },
|
|
32
|
+
access: { posts: { list: 'public', get: 'public' } },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type App = typeof app
|
|
36
|
+
Bun.serve({ fetch: app.handler })
|
|
37
|
+
|
|
38
|
+
Database adapters are imported from their own entry points: libsql(),
|
|
39
|
+
pglite(), bunSql(), postgresJs(). Provisioning: `await provision(app)` pushes
|
|
40
|
+
the schema in development and applies committed migrations once a migrations/
|
|
41
|
+
folder exists.
|
|
42
|
+
|
|
43
|
+
DECLARING AN API
|
|
44
|
+
|
|
45
|
+
Declare the builder once at module scope. defineApi infers the types from the
|
|
46
|
+
values you pass, so you never write the generic parameters yourself. It reads
|
|
47
|
+
nothing at runtime.
|
|
48
|
+
|
|
49
|
+
// src/api/base.ts
|
|
50
|
+
import { defineApi } from 'bunderstack'
|
|
51
|
+
import { envSchema } from '../env'
|
|
52
|
+
import { schema } from '../schema'
|
|
53
|
+
|
|
54
|
+
export const o = defineApi({ schema, env: envSchema })
|
|
55
|
+
export const publicProcedure = o.public
|
|
56
|
+
export const protectedProcedure = o.protected
|
|
57
|
+
|
|
58
|
+
Router modules are plain objects that import the base they need:
|
|
59
|
+
|
|
60
|
+
// src/api/boards.ts
|
|
61
|
+
import { protectedProcedure } from './base'
|
|
62
|
+
|
|
63
|
+
export const boardsRouter = {
|
|
64
|
+
stats: protectedProcedure
|
|
65
|
+
.route({ method: 'GET', path: '/api/board-stats' })
|
|
66
|
+
.input(v.object({ boardId: v.string() }))
|
|
67
|
+
.handler(async ({ context, input }) => countTodos(context.db, input.boardId)),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/api/index.ts
|
|
71
|
+
export const api = { boards: boardsRouter }
|
|
72
|
+
|
|
73
|
+
// config
|
|
74
|
+
createBunderstack({ schema, database, api })
|
|
75
|
+
|
|
76
|
+
Do NOT write a factory that receives a bag of procedures. That pattern exists
|
|
77
|
+
only because the api option used to be a callback. The callback form still
|
|
78
|
+
works — api: (o) => ({ ... }) — for a router that must be built from the
|
|
79
|
+
framework builder at configuration time, but the object form is the default.
|
|
80
|
+
|
|
81
|
+
BASES
|
|
82
|
+
|
|
83
|
+
o.public session resolved only if the handler calls context.getSession()
|
|
84
|
+
o.protected resolves the session, narrows context.user to non-null
|
|
85
|
+
o.webhook public, and preserves the exact raw request body
|
|
86
|
+
o.middleware(fn) a standalone middleware typed over the request context
|
|
87
|
+
|
|
88
|
+
Extend a base with .use(). Whatever you pass to next({ context }) is merged and
|
|
89
|
+
typed downstream:
|
|
90
|
+
|
|
91
|
+
export const adminProcedure = o.protected.use(async ({ context, next, errors }) => {
|
|
92
|
+
if (context.user.role !== 'admin') throw errors.FORBIDDEN({ message: 'Admin only' })
|
|
93
|
+
return next()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
HANDLER CONTEXT
|
|
97
|
+
|
|
98
|
+
db typed Drizzle instance for your schema
|
|
99
|
+
env validated environment, typed from the env schema
|
|
100
|
+
storage StorageFacade: delete, bucket, sweep, getUrl, upload
|
|
101
|
+
email send()
|
|
102
|
+
jobs enqueue()
|
|
103
|
+
realtime publish()
|
|
104
|
+
auth the Better Auth instance
|
|
105
|
+
request the original Request
|
|
106
|
+
resHeaders response headers you can set
|
|
107
|
+
getSession() resolves the session, memoized per request
|
|
108
|
+
peekSession() the already-resolved session or undefined; never resolves
|
|
109
|
+
getRawBody() the exact bytes, memoized; safe for signature checks
|
|
110
|
+
|
|
111
|
+
o.protected additionally guarantees context.user and
|
|
112
|
+
context.session.activeOrganizationId.
|
|
113
|
+
|
|
114
|
+
MIDDLEWARE
|
|
115
|
+
|
|
116
|
+
Two placements, and the difference is the thing people get wrong.
|
|
117
|
+
|
|
118
|
+
.use() on a base reaches only procedures built from that base. Use it for
|
|
119
|
+
rules about a group of procedures: role checks, organization scope, quotas.
|
|
120
|
+
|
|
121
|
+
middleware: [...] in createBunderstack reaches EVERY procedure, including the
|
|
122
|
+
generated CRUD, storage, realtime, and health. Use it for observability. A
|
|
123
|
+
tracing middleware attached to a base leaves generated CRUD unmeasured,
|
|
124
|
+
because the framework builds those procedures itself and they never pass
|
|
125
|
+
through an application base.
|
|
126
|
+
|
|
127
|
+
export const instrumentation = o.middleware(async ({ context, next, path }) => {
|
|
128
|
+
const name = path.join('.')
|
|
129
|
+
const startedAt = performance.now()
|
|
130
|
+
try {
|
|
131
|
+
const result = await next()
|
|
132
|
+
metrics.record(name, performance.now() - startedAt)
|
|
133
|
+
return result
|
|
134
|
+
} catch (error) {
|
|
135
|
+
metrics.error(name, error)
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
createBunderstack({ schema, database, middleware: [instrumentation], api })
|
|
141
|
+
|
|
142
|
+
Rules for graph-wide middleware:
|
|
143
|
+
- It runs before authentication. context.user does not exist there.
|
|
144
|
+
- Read the caller with context.peekSession(), after await next(). Do not call
|
|
145
|
+
getSession(): that forces resolution on every request and makes signed
|
|
146
|
+
webhooks pay for authentication they do not need.
|
|
147
|
+
- peekSession() is for observability only, never for authorization. An
|
|
148
|
+
anonymous request and an unresolved session look identical.
|
|
149
|
+
- A realtime subscription is one long-lived call. Code after await next() runs
|
|
150
|
+
when the stream closes, not when it starts. Filter by path[0] === 'realtime'
|
|
151
|
+
when that matters.
|
|
152
|
+
- Middlewares run outermost first, in array order.
|
|
153
|
+
|
|
154
|
+
TYPED ERRORS
|
|
155
|
+
|
|
156
|
+
Declared codes: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT,
|
|
157
|
+
PAYLOAD_TOO_LARGE, TOO_MANY_REQUESTS. Each maps to its standard HTTP status.
|
|
158
|
+
|
|
159
|
+
Inside a handler or middleware, raise from the errors argument:
|
|
160
|
+
|
|
161
|
+
throw errors.NOT_FOUND({ message: 'Board not found' })
|
|
162
|
+
throw errors.CONFLICT({ message: 'Already running', data: { details: { id } } })
|
|
163
|
+
|
|
164
|
+
Outside a handler — a service function, a job — there is no errors argument.
|
|
165
|
+
Throw BunderstackError; the framework maps it to the same typed error:
|
|
166
|
+
|
|
167
|
+
import { BunderstackError } from 'bunderstack'
|
|
168
|
+
throw new BunderstackError('FORBIDDEN', 'Insufficient credits')
|
|
169
|
+
|
|
170
|
+
Do not construct ORPCError by hand. Clients narrow on these with the oRPC
|
|
171
|
+
isDefinedError helpers.
|
|
172
|
+
|
|
173
|
+
GENERATED CRUD
|
|
174
|
+
|
|
175
|
+
Per exposed table: list, get, create, update, delete. HTTP equivalents are
|
|
176
|
+
GET /api/:table, GET /api/:table/:id, POST /api/:table, PATCH /api/:table/:id,
|
|
177
|
+
DELETE /api/:table/:id.
|
|
178
|
+
|
|
179
|
+
List parameters, identical over RPC and query string: limit (default 20, capped
|
|
180
|
+
at 200), offset, sort, order, q, count, cursor, filters. filters[col]=value is
|
|
181
|
+
equality, filters[col][]=a&filters[col][]=b is IN, filters[col]=null is IS
|
|
182
|
+
NULL. A bare ?col=value is not a filter and returns 400. cursor and offset
|
|
183
|
+
cannot be combined.
|
|
184
|
+
|
|
185
|
+
List response: { items, limit, offset, hasMore, total, sort, order, nextCursor }.
|
|
186
|
+
total is present only with count: true.
|
|
187
|
+
|
|
188
|
+
Exposure and rules come from the access option:
|
|
189
|
+
|
|
190
|
+
access: defineAccess(schema, {
|
|
191
|
+
posts: {
|
|
192
|
+
list: 'authenticated',
|
|
193
|
+
get: 'owner',
|
|
194
|
+
create: 'authenticated',
|
|
195
|
+
update: 'owner',
|
|
196
|
+
delete: 'owner',
|
|
197
|
+
filterableColumns: ['authorId'],
|
|
198
|
+
sortableColumns: ['createdAt'],
|
|
199
|
+
defaultSort: { column: 'createdAt', order: 'desc' },
|
|
200
|
+
scope: { read: (ctx) => ({ userId: ctx.user?.id ?? '' }) },
|
|
201
|
+
},
|
|
202
|
+
auditLog: { crud: false },
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
Rules are 'public', 'authenticated', 'owner', 'deny', or a predicate. Without
|
|
206
|
+
an access entry, a table with a userId column is exposed by convention. Owner
|
|
207
|
+
checks use ownerColumn, detected as userId unless stated.
|
|
208
|
+
|
|
209
|
+
LIST ENDPOINTS OUTSIDE CRUD
|
|
210
|
+
|
|
211
|
+
listSpec gives a procedure you write the same list contract, for tables that
|
|
212
|
+
are not exposed as CRUD or that need a different policy:
|
|
213
|
+
|
|
214
|
+
import { listSpec } from 'bunderstack'
|
|
215
|
+
|
|
216
|
+
const logsList = listSpec(appLogs, {
|
|
217
|
+
filterable: ['level', 'userId'],
|
|
218
|
+
sortable: ['createdAt'],
|
|
219
|
+
defaultSort: { column: 'createdAt', order: 'desc' },
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
export const adminRouter = {
|
|
223
|
+
logs: adminProcedure.input(logsList.input).handler(logsList.handler),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
It returns the schema and the handler separately, not a finished procedure.
|
|
227
|
+
That is deliberate: the base procedure must stay concrete at the call site, or
|
|
228
|
+
TypeScript resolves the builder through a generic constraint and the row type
|
|
229
|
+
is erased. listSpec reads no access configuration; the base carries the policy.
|
|
230
|
+
|
|
231
|
+
TYPING HELPERS
|
|
232
|
+
|
|
233
|
+
A service function in its own module cannot use typeof app.db without an import
|
|
234
|
+
cycle. Use the exported types:
|
|
235
|
+
|
|
236
|
+
import type { BunderstackDb, BunderstackTx } from 'bunderstack'
|
|
237
|
+
import type { schema } from './schema'
|
|
238
|
+
|
|
239
|
+
type Db = BunderstackDb<typeof schema>
|
|
240
|
+
type Tx = BunderstackTx<typeof schema>
|
|
241
|
+
|
|
242
|
+
WEBHOOKS AND HTTP
|
|
243
|
+
|
|
244
|
+
A webhook is an ordinary procedure with a route. getRawBody() returns the exact
|
|
245
|
+
bytes, so signature verification is correct, and the session is never resolved
|
|
246
|
+
unless the handler asks for it:
|
|
247
|
+
|
|
248
|
+
stripeWebhook: o.webhook
|
|
249
|
+
.route({ method: 'POST', path: '/webhooks/stripe' })
|
|
250
|
+
.handler(async ({ context }) => {
|
|
251
|
+
const raw = await context.getRawBody()
|
|
252
|
+
verify(raw, context.request.headers.get('stripe-signature'), context.env.STRIPE_SECRET)
|
|
253
|
+
return { received: true }
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
For typed headers, query parameters, status codes, or response headers, use
|
|
257
|
+
oRPC inputStructure: 'detailed' and outputStructure.
|
|
258
|
+
|
|
259
|
+
ENV
|
|
260
|
+
|
|
261
|
+
env: { server: { STRIPE_KEY: v.string() }, client: { PUBLIC_NAME: v.string() } }
|
|
262
|
+
|
|
263
|
+
Server variables must not start with PUBLIC_; client variables must. Validated
|
|
264
|
+
at boot; app.env and context.env are typed from the schema. Declare the schema
|
|
265
|
+
in its own module so both createBunderstack and defineApi can use it.
|
|
266
|
+
|
|
267
|
+
AUTH
|
|
268
|
+
|
|
269
|
+
auth takes Better Auth options directly, or defineAuth(schema, ({ db, env }) =>
|
|
270
|
+
options) when database hooks need the app's own connection. Better Auth owns
|
|
271
|
+
/api/auth/*. context.user carries id, email, name, and role. authResolver
|
|
272
|
+
replaces session reading with your own implementation.
|
|
273
|
+
|
|
274
|
+
BACKGROUND JOBS
|
|
275
|
+
|
|
276
|
+
One table, one loop. A cron is a job created on a schedule.
|
|
277
|
+
|
|
278
|
+
jobs: (j) => j.define({
|
|
279
|
+
sendEmail: j.job({
|
|
280
|
+
input: v.object({ userId: v.string() }),
|
|
281
|
+
retries: 3,
|
|
282
|
+
handler: async ({ userId }, ctx) => { /* ctx.db, ctx.email, ... */ },
|
|
283
|
+
}),
|
|
284
|
+
daily: j.cron({ schedule: '0 9 * * *', handler: async (_inv, ctx) => {} }),
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
Enqueue with app.jobs.enqueue(name, input, { dedupeKey }). The background loop
|
|
288
|
+
starts on its own, gated by BUNDERSTACK_ROLE: all (default), web, or worker.
|
|
289
|
+
|
|
290
|
+
STORAGE
|
|
291
|
+
|
|
292
|
+
storage: {
|
|
293
|
+
local: './uploads',
|
|
294
|
+
defaultBucket: 'files',
|
|
295
|
+
buckets: {
|
|
296
|
+
avatars: {
|
|
297
|
+
visibility: 'public',
|
|
298
|
+
access: { create: 'authenticated', get: 'public', delete: 'owner' },
|
|
299
|
+
upload: { maxSize: '2mb', accept: ['image/jpeg', 'image/png'] },
|
|
300
|
+
transforms: true,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
Canonical URL: /api/files/{bucket}/{+path}. transforms: true enables on-the-fly
|
|
306
|
+
image derivatives through sharp. app.storage exposes delete, bucket, sweep,
|
|
307
|
+
getUrl, and upload for server-generated files.
|
|
308
|
+
|
|
309
|
+
REALTIME
|
|
310
|
+
|
|
311
|
+
realtime: true uses an in-memory publisher; { redis: url } fans out across
|
|
312
|
+
processes. Generated writes publish automatically. After a custom write,
|
|
313
|
+
publish the complete returned row:
|
|
314
|
+
|
|
315
|
+
await context.realtime.publish(schema.posts, 'update', post)
|
|
316
|
+
|
|
317
|
+
Clients subscribe through the same graph; there is no separate SSE transport to
|
|
318
|
+
configure.
|
|
319
|
+
|
|
320
|
+
CLIENT
|
|
321
|
+
|
|
322
|
+
import { createClient } from 'bunderstack-query'
|
|
323
|
+
import type { App } from './bunderstack'
|
|
324
|
+
|
|
325
|
+
export const api = createClient<App>({ queryClient, realtime: true })
|
|
326
|
+
|
|
327
|
+
await api.posts.list.call({ limit: 20 })
|
|
328
|
+
useQuery(api.posts.list.queryOptions({ input: { limit: 20 } }))
|
|
329
|
+
useMutation(api.posts.create.mutationOptions())
|
|
330
|
+
await api.files.avatars.upload.call(file)
|
|
331
|
+
api.files.avatars.url(fileId, { width: 320, format: 'webp' })
|
|
332
|
+
|
|
333
|
+
App is a type-only import, so server code never enters the browser bundle.
|
|
334
|
+
bunderstack-sync layers TanStack DB collections over the same procedures.
|
|
335
|
+
|
|
336
|
+
CONFIG KEYS
|
|
337
|
+
|
|
338
|
+
schema (required), database (required: adapter, url), access, auth,
|
|
339
|
+
authResolver, storage, email, env, jobs, api, middleware, realtime, rateLimit,
|
|
340
|
+
idempotency, background, openapi, processEnv.
|
|
341
|
+
|
|
342
|
+
OPENAPI
|
|
343
|
+
|
|
344
|
+
openapi: true serves /api/openapi.json. RPC types remain the source of truth; a
|
|
345
|
+
procedure without .output() has an unspecified response body. OpenAPI failures
|
|
346
|
+
never break normal boot.
|
|
347
|
+
|
|
348
|
+
COMMON MISTAKES
|
|
349
|
+
|
|
350
|
+
- Writing router factories that take a procedure bag. Use module-scope bases.
|
|
351
|
+
- Declaring middleware with os.$context<...>(). Use o.middleware(...).
|
|
352
|
+
- Attaching observability to a base and expecting it to cover generated CRUD.
|
|
353
|
+
Use the middleware config option.
|
|
354
|
+
- Calling getSession() in graph-wide middleware. Use peekSession().
|
|
355
|
+
- Constructing ORPCError by hand. Use errors.CODE() or BunderstackError.
|
|
356
|
+
- Using any for a db parameter. Use BunderstackDb<typeof schema>.
|
|
357
|
+
- Importing the app from a module the app imports. Both the api router and any
|
|
358
|
+
module it pulls in are evaluated at import time, so a cycle through the app
|
|
359
|
+
breaks initialization. Load the app lazily there instead.
|
|
360
|
+
- Reading env from an imported module inside a handler. Use context.env.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunderstack",
|
|
3
|
-
"version": "0.17.0
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, and validated env from one config.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
|
+
"skills",
|
|
29
|
+
"llms.txt",
|
|
28
30
|
"CHANGELOG.md",
|
|
29
31
|
"README.md",
|
|
30
32
|
"LICENSE"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: creating-bunderstack-apps
|
|
3
|
+
description: Use when working in a repository that depends on bunderstack - starting or structuring the application, adding or changing oRPC procedures, bases, middleware, access rules, jobs, storage, or realtime, choosing a runtime integration, or preparing it for production.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Creating Bunderstack Apps
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
1. Inspect the product brief and target runtime.
|
|
11
|
+
2. Choose the layout from the table below.
|
|
12
|
+
3. For a full TanStack Start SaaS, copy `templates/tanstack-start-saas/`.
|
|
13
|
+
4. Configure schema, access, auth, env, storage, jobs, realtime, and the oRPC API graph.
|
|
14
|
+
5. Mount the single `app.handler` integration.
|
|
15
|
+
6. Add committed migrations and a deployment blueprint before production.
|
|
16
|
+
7. Run the verification contract.
|
|
17
|
+
|
|
18
|
+
| Condition | Layout |
|
|
19
|
+
| --------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
20
|
+
| Small API with short configuration | `src/bunderstack.ts` |
|
|
21
|
+
| Auth, access, jobs, env, or custom oRPC procedures need independent modules | `src/bunderstack/` |
|
|
22
|
+
| Full SaaS | Copy `templates/tanstack-start-saas/` and keep its modular layout |
|
|
23
|
+
|
|
24
|
+
## Runtime decision recipe
|
|
25
|
+
|
|
26
|
+
Choose the process boundary before listing files. A TanStack Start SaaS keeps
|
|
27
|
+
the API mount in Start. A standalone Bun API has `src/bunderstack.ts` (or the
|
|
28
|
+
modular `src/bunderstack/` entry) and a Bun process that delegates to
|
|
29
|
+
`app.handler`.
|
|
30
|
+
|
|
31
|
+
For a React SPA brief, use the browser-only layout: an `api/` project contains
|
|
32
|
+
the Bunderstack entry and Bun API process, and a separate `frontend/` project
|
|
33
|
+
contains the React build. The API process owns `app.handler`; the browser
|
|
34
|
+
client receives a configured API base URL and calls that process. Include both
|
|
35
|
+
processes and the configured browser-to-API path in the proposed structure.
|
|
36
|
+
|
|
37
|
+
`node_modules/bunderstack/llms.txt` is a dense plain-text reference for the
|
|
38
|
+
whole framework. Read it when a detail is not covered here.
|
|
39
|
+
|
|
40
|
+
Read [application structure](references/application-structure.md) before placing
|
|
41
|
+
the Bunderstack entry, schemas, authorization, or configuration modules, and
|
|
42
|
+
before declaring procedures, bases, middleware, or errors.
|
|
43
|
+
Read [runtime integrations](references/runtime-integrations.md) before mounting
|
|
44
|
+
HTTP, adding a worker, or choosing a framework adapter. Read the
|
|
45
|
+
[verification contract](references/verification.md) after adding the app
|
|
46
|
+
scripts and again before handoff or deployment.
|