@frontera-sdk/cli 1.43.10 → 1.44.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 +140 -12
- package/package.json +3 -3
- package/src/adopt.ts +436 -0
- package/src/api/apps-api.ts +30 -0
- package/src/api/blueprint-authoring-api.ts +13 -2
- package/src/api/governed-action-api.ts +192 -0
- package/src/api/platform-api.ts +4 -0
- package/src/blueprint/ontology-edit-plan.ts +195 -0
- package/src/blueprint-types.ts +252 -0
- package/src/commands/action/deploy.ts +135 -0
- package/src/commands/action/grant.ts +68 -0
- package/src/commands/action/index-commands.ts +29 -0
- package/src/commands/action/list.ts +49 -0
- package/src/commands/action/prepare.ts +48 -0
- package/src/commands/action/review.ts +94 -0
- package/src/commands/app/deploy.ts +16 -5
- package/src/commands/app/dev.ts +173 -0
- package/src/commands/app/init.ts +270 -28
- package/src/commands/app/sdk.ts +31 -0
- package/src/commands/app/versions.ts +8 -1
- package/src/commands/blueprint/editable.ts +151 -0
- package/src/commands/blueprint/generate-types.ts +58 -0
- package/src/commands/blueprint/get.ts +29 -34
- package/src/commands/blueprint/list.ts +2 -1
- package/src/commands/registry.ts +12 -0
- package/src/context.ts +4 -4
- package/src/dev-broker.ts +71 -0
- package/src/flag-help.ts +24 -1
- package/src/heal.ts +37 -2
- package/src/manifest.ts +89 -8
- package/src/packaging.ts +6 -0
- package/src/project-bootstrap.ts +176 -0
- package/src/project.ts +68 -35
- package/src/provenance.ts +89 -0
- package/src/render-evidence.ts +28 -0
- package/src/sdk-sync.ts +41 -0
- package/src/shadcn-components.ts +106 -0
- package/src/static-app-validation.ts +67 -0
- package/src/template.ts +211 -32
- package/src/templates/next-app-files.ts +1052 -0
- package/src/templates/next-skills.ts +1216 -0
- package/src/vendor/sdk-sources.json +21 -15
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pattern skills shipped inside every scaffolded App.
|
|
3
|
+
*
|
|
4
|
+
* These are written to the same anatomy as the in-repo Sebati skills, because
|
|
5
|
+
* that anatomy is what makes a skill usable by a small model: a description
|
|
6
|
+
* that says WHEN to load it, rules stated as decisions rather than advice, one
|
|
7
|
+
* excerpt copied from code that actually exists in the project, the naming
|
|
8
|
+
* conventions, an explicit red-flag list, and the command that proves the work.
|
|
9
|
+
*
|
|
10
|
+
* Two constraints keep them honest:
|
|
11
|
+
*
|
|
12
|
+
* - Every `frontera …` invocation and every `--flag` named here is asserted
|
|
13
|
+
* against the real command registry by `skill-accuracy.test.ts`. A skill
|
|
14
|
+
* that advertises a command nobody implemented has happened twice in this
|
|
15
|
+
* repo; it is now a test failure rather than a review question.
|
|
16
|
+
* - Every path named here is a path the scaffold writes. A rule pointing at a
|
|
17
|
+
* directory that does not exist teaches an agent to invent one.
|
|
18
|
+
*
|
|
19
|
+
* Lines are authored as arrays of strings so markdown backticks need no
|
|
20
|
+
* escaping — these files are mostly code spans, and an escaped skill is an
|
|
21
|
+
* unreadable skill.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface Skill {
|
|
25
|
+
name: string
|
|
26
|
+
description: string
|
|
27
|
+
body: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Skill file map, keyed by path relative to the project root. */
|
|
31
|
+
export function nextSkillFiles(): Record<string, string> {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
SKILLS.map((skill) => [
|
|
34
|
+
`.agents/skills/${skill.name}/SKILL.md`,
|
|
35
|
+
`---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.body.join('\n')}\n`,
|
|
36
|
+
]),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Names of the shipped skills, in dispatcher order. */
|
|
41
|
+
export const NEXT_SKILL_NAMES = [
|
|
42
|
+
'using-frontera-app-patterns',
|
|
43
|
+
'frontera-app-project',
|
|
44
|
+
'frontera-app-routing',
|
|
45
|
+
'frontera-app-session',
|
|
46
|
+
'frontera-blueprint-data',
|
|
47
|
+
'frontera-blueprint-actions',
|
|
48
|
+
'frontera-app-ui',
|
|
49
|
+
'frontera-design-tokens',
|
|
50
|
+
'frontera-ui-components',
|
|
51
|
+
'frontera-query-ui-states',
|
|
52
|
+
'frontera-app-tables',
|
|
53
|
+
'frontera-app-testing',
|
|
54
|
+
'frontera-app-security',
|
|
55
|
+
'frontera-app-release',
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
const dispatcher: Skill = {
|
|
59
|
+
name: 'using-frontera-app-patterns',
|
|
60
|
+
description:
|
|
61
|
+
'Use at the START of any task in a Frontera App — a Next.js project with frontera.config.json that deploys through the frontera CLI. Establishes the architecture, names the reference feature to copy, and routes you to the pattern skill for the layer you are touching.',
|
|
62
|
+
body: [
|
|
63
|
+
'# Frontera App patterns',
|
|
64
|
+
'',
|
|
65
|
+
'The dispatcher. Load this first, then the skill for the layer you touch. These patterns',
|
|
66
|
+
'are already followed by the code in this project — match them, do not start a second style.',
|
|
67
|
+
'',
|
|
68
|
+
'## Am I in a Frontera App?',
|
|
69
|
+
'',
|
|
70
|
+
'Yes if the project has ANY of: `frontera.config.json`, `@frontera-sdk/core` in',
|
|
71
|
+
'`package.json`, a `.frontera/` directory, or `.agents/skills/frontera-app-*/`.',
|
|
72
|
+
'',
|
|
73
|
+
'## The reference feature — read it before writing anything',
|
|
74
|
+
'',
|
|
75
|
+
'One complete vertical slice ships with the scaffold, and every rule below is visible in it:',
|
|
76
|
+
'',
|
|
77
|
+
'```text',
|
|
78
|
+
'src/app/page.tsx thin route adapter',
|
|
79
|
+
'src/ui/app-shell/app-shell.tsx the frame, aware of embedded vs standalone',
|
|
80
|
+
'src/ui/object-explorer/ the feature folder',
|
|
81
|
+
'src/lib/blueprint/objects/objects-hooks.ts the data layer + pure helpers',
|
|
82
|
+
'src/lib/blueprint/objects/__tests__/ the tests those helpers earn',
|
|
83
|
+
'```',
|
|
84
|
+
'',
|
|
85
|
+
'Copying that shape is faster and more accurate than reasoning from these skills alone.',
|
|
86
|
+
'',
|
|
87
|
+
'## Invariants — all layers',
|
|
88
|
+
'',
|
|
89
|
+
'1. **Bun runs everything.** `bun install`, `bun run <script>`, `bun test`. Never npm/yarn/pnpm.',
|
|
90
|
+
'2. **The result is static files.** `bun run build` must emit `out/index.html`. There is no',
|
|
91
|
+
' server runtime: no Server Actions, no request-time Route Handlers, no middleware.',
|
|
92
|
+
'3. **One concern per file.** One React component per file under `src/ui/`. Data access and',
|
|
93
|
+
' components never share a file. No `index.ts` barrels anywhere.',
|
|
94
|
+
'4. **Components take props; hooks fetch.** Every Blueprint read lives in',
|
|
95
|
+
' `src/lib/blueprint/<domain>/`.',
|
|
96
|
+
'5. **The server filters, sorts, cursor-pages and counts.** Narrowing a fetched page is the single',
|
|
97
|
+
' most common way an App silently lies about the data.',
|
|
98
|
+
'6. **Browser code holds no secret.** Not in source, not in `NEXT_PUBLIC_*`, not in `.env`.',
|
|
99
|
+
'7. **User-visible strings come from `src/lib/copy.ts`.**',
|
|
100
|
+
'',
|
|
101
|
+
'## Router — load the skill for what you are touching',
|
|
102
|
+
'',
|
|
103
|
+
'| Working on | Path signal | Skill |',
|
|
104
|
+
'|---|---|---|',
|
|
105
|
+
'| Structure, dependencies, scripts, build config | `package.json`, `next.config.ts` | **frontera-app-project** |',
|
|
106
|
+
'| Pages, links, deep links, record selection | `src/app/**/page.tsx` | **frontera-app-routing** |',
|
|
107
|
+
'| Session, host integration, local development | `src/app/providers.tsx` | **frontera-app-session** |',
|
|
108
|
+
'| Reading objects, filters, paging, aggregates | `src/lib/blueprint/` | **frontera-blueprint-data** |',
|
|
109
|
+
'| Changing data — submitting an Action, approvals | `src/lib/blueprint/` | **frontera-blueprint-actions** |',
|
|
110
|
+
'| Feature folders, composition, responsiveness | `src/ui/<feature>/` | **frontera-app-ui** |',
|
|
111
|
+
'| Colors, elevation, dark mode, CSS variables | `src/app/globals.css` | **frontera-design-tokens** |',
|
|
112
|
+
'| Shared primitives, shadcn components | `src/components/ui/` | **frontera-ui-components** |',
|
|
113
|
+
'| Loading, empty, error, stale rendering | any query consumer | **frontera-query-ui-states** |',
|
|
114
|
+
'| Tables, server paging, column definitions | `*-table.tsx`, `*-columns.ts` | **frontera-app-tables** |',
|
|
115
|
+
'| Tests | `src/**/__tests__/` | **frontera-app-testing** |',
|
|
116
|
+
'| External requests, CSP, secrets, iframe | `frontera.config.json` | **frontera-app-security** |',
|
|
117
|
+
'| Build, preview, promote, rollback | deployment | **frontera-app-release** |',
|
|
118
|
+
'',
|
|
119
|
+
'A feature touches several layers — load in dependency order: project (where) → blueprint-data',
|
|
120
|
+
'(reads) → ui (shape) → query-ui-states/tables (rendering) → release (ship). Do not load all of them.',
|
|
121
|
+
'',
|
|
122
|
+
'For the CLI rather than the code — logging in, discovering data, packaging, deploying — load',
|
|
123
|
+
'`.agents/skills/using-frontera-cli/SKILL.md` when it is present.',
|
|
124
|
+
'',
|
|
125
|
+
'## The only write path',
|
|
126
|
+
'',
|
|
127
|
+
'Blueprint Actions. There is no direct object mutation, and no service route to reach around',
|
|
128
|
+
'them because the browser can construct a URL. Reads are `useObjects` and friends; writes are',
|
|
129
|
+
'`useSubmitAction` — see **frontera-blueprint-actions**.',
|
|
130
|
+
'',
|
|
131
|
+
'## Verify before claiming done',
|
|
132
|
+
'',
|
|
133
|
+
'```bash',
|
|
134
|
+
'bun run check',
|
|
135
|
+
'```',
|
|
136
|
+
'',
|
|
137
|
+
'That runs typecheck, then tests, then the build. For anything touching data, also run it',
|
|
138
|
+
'against real data:',
|
|
139
|
+
'',
|
|
140
|
+
'```bash',
|
|
141
|
+
'frontera app dev',
|
|
142
|
+
'```',
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const project: Skill = {
|
|
147
|
+
name: 'frontera-app-project',
|
|
148
|
+
description:
|
|
149
|
+
'Use when changing project structure, dependencies, scripts, or build configuration in a Frontera App — which file owns which concern, what a static export forbids, and which command is the gate before claiming done.',
|
|
150
|
+
body: [
|
|
151
|
+
'# Project contract',
|
|
152
|
+
'',
|
|
153
|
+
'The build happens on a laptop or in customer CI. Frontera serves an uploaded immutable',
|
|
154
|
+
'artifact; it never installs dependencies and never runs Next.',
|
|
155
|
+
'',
|
|
156
|
+
'## Rules',
|
|
157
|
+
'',
|
|
158
|
+
'- Files own separate concerns and do not borrow from each other:',
|
|
159
|
+
' - `package.json` — package identity, scripts, dependencies, `packageManager`. No `frontera` field.',
|
|
160
|
+
' - `frontera.config.json` — `displayName`, `outputDirectory`, `routing`, `connectDomains`, `resourceDomains`.',
|
|
161
|
+
' - `next.config.ts` — static-export behaviour.',
|
|
162
|
+
' - `.frontera/state.json` — environment binding written by the CLI. Gitignored; never author it.',
|
|
163
|
+
' - `.env*` — public build values only. A credential here ships to the browser.',
|
|
164
|
+
'- Keep the required Next configuration: `output: "export"`, `trailingSlash: true`,',
|
|
165
|
+
' `images.unoptimized: true`, and `transpilePackages` for both SDK packages. The artifact',
|
|
166
|
+
' contract is `outputDirectory: "out"` with `routing: "filesystem"` — it describes files, not a framework.',
|
|
167
|
+
'- Use the public versioned `@frontera-sdk/core` and `@frontera-sdk/blueprint` packages. Do not',
|
|
168
|
+
' vendor, copy or patch SDK source into a Next App — that is the legacy Vite scaffold only.',
|
|
169
|
+
'- Scripts are a fixed set with fixed meanings. `dev` MUST stay `next dev`: `frontera app dev`',
|
|
170
|
+
' spawns `bun run dev` and passes the host and port it chose.',
|
|
171
|
+
'- Add a dependency only if it runs in a browser bundle. No `node:` built-ins, no server SDKs,',
|
|
172
|
+
' no package that needs a secret to be useful.',
|
|
173
|
+
'- `bun run check` is the gate. `next build` does not type-check the project and `tsc` never',
|
|
174
|
+
' sees the export constraints, so neither one alone proves anything.',
|
|
175
|
+
'- No linter ships with the scaffold. If a project adds one, wire it into `check` in the same',
|
|
176
|
+
' commit, or it will never run again.',
|
|
177
|
+
'',
|
|
178
|
+
'```jsonc',
|
|
179
|
+
'// package.json',
|
|
180
|
+
'"scripts": {',
|
|
181
|
+
' "dev": "next dev", // spawned by `frontera app dev` — do not rename',
|
|
182
|
+
' "build": "next build", // must produce out/index.html',
|
|
183
|
+
' "typecheck": "tsc --noEmit",',
|
|
184
|
+
' "test": "bun test src",',
|
|
185
|
+
' "check": "bun run typecheck && bun run test && bun run build",',
|
|
186
|
+
' "blueprint:types": "frontera blueprint generate-types",',
|
|
187
|
+
' "blueprint:types:check": "frontera blueprint generate-types --check",',
|
|
188
|
+
' "deploy": "bun run check && frontera app deploy"',
|
|
189
|
+
'}',
|
|
190
|
+
'```',
|
|
191
|
+
'',
|
|
192
|
+
'## Unsupported',
|
|
193
|
+
'',
|
|
194
|
+
'Middleware and proxy files, Server Actions, request-time Route Handlers, request-time',
|
|
195
|
+
'rendering, a custom Node server, optimized Next Image endpoints, and runtime-only dynamic',
|
|
196
|
+
'routes. Each of these builds locally and fails on the deployed artifact, which is the worst',
|
|
197
|
+
'possible time to find out.',
|
|
198
|
+
'',
|
|
199
|
+
'## Naming',
|
|
200
|
+
'',
|
|
201
|
+
'Filenames kebab-case (`object-table.tsx`, `use-debounced-value.ts`). Exports PascalCase for',
|
|
202
|
+
'components and types, camelCase for hooks, functions and constants. Import through the `@/`',
|
|
203
|
+
'alias, always to an explicit file.',
|
|
204
|
+
'',
|
|
205
|
+
'## Red Flags — STOP',
|
|
206
|
+
'',
|
|
207
|
+
'- A `frontera` key added to `package.json`, or hosting configuration in `next.config.ts`.',
|
|
208
|
+
'- Renaming or removing the `dev` script.',
|
|
209
|
+
'- `@frontera-sdk/*` source copied into `src/`.',
|
|
210
|
+
'- A dependency that needs a server, a secret, or a Node built-in.',
|
|
211
|
+
'- Claiming done after `bun run build` alone.',
|
|
212
|
+
'',
|
|
213
|
+
'## Verification',
|
|
214
|
+
'',
|
|
215
|
+
'```bash',
|
|
216
|
+
'bun run check',
|
|
217
|
+
'```',
|
|
218
|
+
'',
|
|
219
|
+
'```bash',
|
|
220
|
+
'ls out/index.html',
|
|
221
|
+
'```',
|
|
222
|
+
'',
|
|
223
|
+
'## Scope',
|
|
224
|
+
'',
|
|
225
|
+
'Applies to every Next.js Frontera App. The legacy Vite scaffold (`frontera app init` with',
|
|
226
|
+
'`--framework react`) vendors SDK source instead and is governed by its own bundled skills.',
|
|
227
|
+
],
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const routing: Skill = {
|
|
231
|
+
name: 'frontera-app-routing',
|
|
232
|
+
description:
|
|
233
|
+
'Use when adding routes, navigation, deep links, or record-selection flows in a Frontera App — fixed exported routes only, search params instead of bracket routes, thin page adapters, and how init.path maps to a route.',
|
|
234
|
+
body: [
|
|
235
|
+
'# Routing',
|
|
236
|
+
'',
|
|
237
|
+
'Only routes that exist as files after `bun run build` are servable. `src/app/shipments/page.tsx`',
|
|
238
|
+
'becomes `out/shipments/index.html` and is served at `/shipments/`.',
|
|
239
|
+
'',
|
|
240
|
+
'## Rules',
|
|
241
|
+
'',
|
|
242
|
+
'- Keep `page.tsx` a thin adapter: read configuration or search params, choose a layout wrapper,',
|
|
243
|
+
' render ONE component from `src/ui/<feature>/<feature>.tsx`. No feature JSX, no hooks, no reads.',
|
|
244
|
+
'- Do not model runtime identity with a bracket route (`src/app/shipments/[id]/`). Nothing',
|
|
245
|
+
' generates those pages at build time. Use a fixed route plus a search parameter',
|
|
246
|
+
' (`/shipments/?id=…`) or client state, and validate and encode the value before it reaches a URL.',
|
|
247
|
+
'- Next owns application routing. Use `Link`, `useRouter` and `useSearchParams` normally.',
|
|
248
|
+
'- `useFronteraApp().navigate(path)` synchronizes the HOST url while embedded. It is deliberately',
|
|
249
|
+
' a no-op standalone and locally. It is not a replacement for `router.push`; call both when a',
|
|
250
|
+
' navigation should also change the platform address bar.',
|
|
251
|
+
'- On embedded startup `init.path` is the platform-owned deep link. Map it to a route your build',
|
|
252
|
+
' actually exported, and give unknown paths an explicit not-found experience.',
|
|
253
|
+
'- A page that reads search params needs a Suspense boundary under static export. Keep the',
|
|
254
|
+
' reading component inside the feature folder so the boundary lives in one place.',
|
|
255
|
+
'',
|
|
256
|
+
'```tsx',
|
|
257
|
+
'// src/app/page.tsx — the whole file',
|
|
258
|
+
"import { STARTER_OBJECT_TYPE, STARTER_SEARCH_PROPERTY } from '@/lib/blueprint/starter'",
|
|
259
|
+
"import { AppShell } from '@/ui/app-shell/app-shell'",
|
|
260
|
+
"import { ObjectExplorer } from '@/ui/object-explorer/object-explorer'",
|
|
261
|
+
'',
|
|
262
|
+
'export default function HomePage() {',
|
|
263
|
+
' return (',
|
|
264
|
+
' <AppShell>',
|
|
265
|
+
' <ObjectExplorer objectType={STARTER_OBJECT_TYPE} searchProperty={STARTER_SEARCH_PROPERTY} />',
|
|
266
|
+
' </AppShell>',
|
|
267
|
+
' )',
|
|
268
|
+
'}',
|
|
269
|
+
'```',
|
|
270
|
+
'',
|
|
271
|
+
'## Naming',
|
|
272
|
+
'',
|
|
273
|
+
'Page component `<Thing>Page`; layout `<Area>Layout`; route directories kebab-case and plural',
|
|
274
|
+
'when they list (`shipments/`). Feature components keep their own names.',
|
|
275
|
+
'',
|
|
276
|
+
'## Red Flags — STOP',
|
|
277
|
+
'',
|
|
278
|
+
'- A bracket route directory.',
|
|
279
|
+
'- Assuming an SPA root fallback exists — a filesystem export has none.',
|
|
280
|
+
'- Feature UI, hooks or data access living under `src/app/`.',
|
|
281
|
+
'- Co-located `*-client.tsx` siblings or `components/` trees under `src/app/`.',
|
|
282
|
+
'- Record identifiers in path segments that no build step generated.',
|
|
283
|
+
'- Two navigation systems with different active-state rules.',
|
|
284
|
+
'',
|
|
285
|
+
'## Verification',
|
|
286
|
+
'',
|
|
287
|
+
'```bash',
|
|
288
|
+
'bun run build',
|
|
289
|
+
'```',
|
|
290
|
+
'',
|
|
291
|
+
'Then confirm each intended route exists as a directory with an `index.html` under `out/`.',
|
|
292
|
+
'',
|
|
293
|
+
'## Scope',
|
|
294
|
+
'',
|
|
295
|
+
'Applies to every Next.js Frontera App. Bracket routes become legal only if a build-time',
|
|
296
|
+
'`generateStaticParams` enumerates them — which requires data at build time that an App',
|
|
297
|
+
'generally does not have.',
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const session: Skill = {
|
|
302
|
+
name: 'frontera-app-session',
|
|
303
|
+
description:
|
|
304
|
+
'Use when working on authentication, App initialization, host integration, or local development in a Frontera App — the single provider mount, the three transports, what useFronteraApp returns, and how to treat the token.',
|
|
305
|
+
body: [
|
|
306
|
+
'# Sessions',
|
|
307
|
+
'',
|
|
308
|
+
'The App has no data scope until a session hands it one. `FronteraAppProvider` completes that',
|
|
309
|
+
'handshake and only then renders children, so no hook below it needs a "not ready" branch.',
|
|
310
|
+
'',
|
|
311
|
+
'## Rules',
|
|
312
|
+
'',
|
|
313
|
+
'- Mount ONE `FronteraAppProvider` in `src/app/providers.tsx`, with domain providers passed as a',
|
|
314
|
+
' list (`providers={[blueprintProvider]}`). A second data domain is one more array entry.',
|
|
315
|
+
'- Never recreate the bridge, the session fetch, or the query client in application code. A',
|
|
316
|
+
' per-route provider stack forks the session and the cache; the symptom is a second handshake',
|
|
317
|
+
' and rows that will not refresh.',
|
|
318
|
+
'- The provider picks exactly one configured transport before connecting:',
|
|
319
|
+
' - `embedded` — validated postMessage bridge to the Frontera parent.',
|
|
320
|
+
' - `standalone` — same-origin `/.frontera/session` backed by App cookies.',
|
|
321
|
+
' - `local` — exact-origin loopback broker started by `frontera app dev`.',
|
|
322
|
+
'- Every mode supplies the same `BridgeInit` and client, so feature code branches on `mode` only',
|
|
323
|
+
' for presentation (chrome density, external links), never for data access.',
|
|
324
|
+
'- Treat `init.token` as opaque and short-lived. Do not cache it, put it in state, log it, send it',
|
|
325
|
+
' anywhere, or retry an authorization failure with a different credential.',
|
|
326
|
+
'- Local development is `frontera app dev`. It mints a short-lived App token, keeps the stored CLI',
|
|
327
|
+
' key in the broker, and owns `NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT` — never hand-write that',
|
|
328
|
+
' variable. `bun run dev` alone starts Next with no session: fine for layout, useless for data.',
|
|
329
|
+
'',
|
|
330
|
+
'```tsx',
|
|
331
|
+
"'use client'",
|
|
332
|
+
'',
|
|
333
|
+
"import { useFronteraApp } from '@frontera-sdk/core/react'",
|
|
334
|
+
'',
|
|
335
|
+
'export function AppShell({ children }: { children: React.ReactNode }) {',
|
|
336
|
+
' const { mode, init } = useFronteraApp()',
|
|
337
|
+
' // mode: "embedded" | "standalone" | "local"',
|
|
338
|
+
' // init: { appId, version, orgId, workspaceId, theme, state, path? }',
|
|
339
|
+
' return <section data-mode={mode}>{children}</section>',
|
|
340
|
+
'}',
|
|
341
|
+
'```',
|
|
342
|
+
'',
|
|
343
|
+
'## Naming',
|
|
344
|
+
'',
|
|
345
|
+
'Root component `Providers` in `src/app/providers.tsx`. Shell component `AppShell` in',
|
|
346
|
+
'`src/ui/app-shell/app-shell.tsx`. Session values are read with `useFronteraApp()`, never threaded',
|
|
347
|
+
'through props.',
|
|
348
|
+
'',
|
|
349
|
+
'## Red Flags — STOP',
|
|
350
|
+
'',
|
|
351
|
+
'- A second `FronteraAppProvider`, or a provider stack under a route layout.',
|
|
352
|
+
'- Reading a token out of context to pass to `fetch`.',
|
|
353
|
+
'- Writing `NEXT_PUBLIC_FRONTERA_DEV_SESSION_ENDPOINT` by hand into `.env`.',
|
|
354
|
+
'- Branching data access on `mode`.',
|
|
355
|
+
'- Rendering the app before the session resolves, or adding a "not ready" guard inside a feature.',
|
|
356
|
+
'',
|
|
357
|
+
'## Verification',
|
|
358
|
+
'',
|
|
359
|
+
'```bash',
|
|
360
|
+
'frontera app dev',
|
|
361
|
+
'```',
|
|
362
|
+
'',
|
|
363
|
+
'The App should mount with real rows. A page that renders but 401s on every read means the',
|
|
364
|
+
'session is not the one you think it is.',
|
|
365
|
+
'',
|
|
366
|
+
'## Scope',
|
|
367
|
+
'',
|
|
368
|
+
'Applies to every Next.js Frontera App. Standalone mode requires the App to be published and',
|
|
369
|
+
'authorized; embedded mode is what the platform surface uses.',
|
|
370
|
+
],
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const blueprintData: Skill = {
|
|
374
|
+
name: 'frontera-blueprint-data',
|
|
375
|
+
description:
|
|
376
|
+
'Use when reading Blueprint data in a Frontera App — the src/lib/blueprint/<domain>/ data layer, useObjects/useObjectInstance/useAggregate, server-side where clauses, paging, totals, and typed contracts from bun run blueprint:types.',
|
|
377
|
+
body: [
|
|
378
|
+
'# Blueprint reads',
|
|
379
|
+
'',
|
|
380
|
+
'Blueprint is the data contract. Every read is scoped by the credential the session handed over,',
|
|
381
|
+
'so an object type this workspace was not granted is simply absent.',
|
|
382
|
+
'',
|
|
383
|
+
'## Rules',
|
|
384
|
+
'',
|
|
385
|
+
'- One folder per domain: `src/lib/blueprint/<domain>/<domain>-hooks.ts`. Never two domains in one',
|
|
386
|
+
' file, never a read inside a component, never an `index.ts` barrel. Consumers import the explicit file.',
|
|
387
|
+
'- Discover what exists before writing a query:',
|
|
388
|
+
'',
|
|
389
|
+
'```bash',
|
|
390
|
+
'frontera blueprint list',
|
|
391
|
+
'```',
|
|
392
|
+
'',
|
|
393
|
+
'```bash',
|
|
394
|
+
'frontera blueprint get <apiName>',
|
|
395
|
+
'```',
|
|
396
|
+
'',
|
|
397
|
+
'- Generate the typed contract, then commit it:',
|
|
398
|
+
'',
|
|
399
|
+
'```bash',
|
|
400
|
+
'bun run blueprint:types',
|
|
401
|
+
'```',
|
|
402
|
+
'',
|
|
403
|
+
' It writes `src/generated/frontera-blueprint.ts`, a deterministic projection of the Blueprint',
|
|
404
|
+
' this workspace is granted. Never edit that file, never copy server ids into source, and rerun',
|
|
405
|
+
' it after a Blueprint publication or a grant change. `bun run blueprint:types:check` is an',
|
|
406
|
+
' authenticated freshness gate for CI — install, build, dev and deploy never contact Blueprint.',
|
|
407
|
+
'- Pass LITERAL api names so inference works: `useObjects("Shipment")` types rows, filters, sorts',
|
|
408
|
+
' and projections from the committed contract. A `string` variable erases all of it.',
|
|
409
|
+
'- Choose the hook by the question: `useObjects` for rows, `useObjectInstance` for one selected',
|
|
410
|
+
' record, `useAggregate` for measures, `useObjectQuery` when the object type is dynamic.',
|
|
411
|
+
'- Filter, order, cursor-page and aggregate on the SERVER. `where` compiles into the object set.',
|
|
412
|
+
'- A total is its own `useAggregate` over the SAME object set — build the filter once and share it.',
|
|
413
|
+
' `rows.length` is the size of a page.',
|
|
414
|
+
'- Use `enabled` for anything depending on a runtime value, and `placeholderData: keepPreviousData`',
|
|
415
|
+
' for paging so the table dims instead of collapsing.',
|
|
416
|
+
'- Keep query inputs serializable and stable; the SDK derives cache keys from the request.',
|
|
417
|
+
'- Extract pure helpers (filter builders, aggregate parsing, formatting) and test them. They are the only',
|
|
418
|
+
' part of a data layer that can be tested without a session.',
|
|
419
|
+
'- Do not collapse forbidden, unknown object type, network failure and a successful empty result',
|
|
420
|
+
' into one empty array. They are four different things to say to a customer.',
|
|
421
|
+
'- This skill is reads. Writes go through Blueprint Actions — see **frontera-blueprint-actions**.',
|
|
422
|
+
'',
|
|
423
|
+
'```ts',
|
|
424
|
+
'// src/lib/blueprint/shipment/shipment-hooks.ts',
|
|
425
|
+
"import { keepPreviousData } from '@tanstack/react-query'",
|
|
426
|
+
"import { useAggregate, useObjects } from '@frontera-sdk/blueprint/hooks'",
|
|
427
|
+
"import { objectsOf } from '@frontera-sdk/blueprint/types'",
|
|
428
|
+
"import type { BlueprintWhereNode } from '@frontera-sdk/blueprint/types'",
|
|
429
|
+
'',
|
|
430
|
+
'export const PAGE_SIZE = 25',
|
|
431
|
+
'',
|
|
432
|
+
"export function delayedWhere(city?: string): BlueprintWhereNode<'Shipment'> {",
|
|
433
|
+
" const status = { property: 'status', op: 'eq', value: 'delayed' } as const",
|
|
434
|
+
" return city ? { and: [status, { property: 'city', op: 'eq', value: city }] } : status",
|
|
435
|
+
'}',
|
|
436
|
+
'',
|
|
437
|
+
'export function useDelayedShipments(pageToken: string | undefined, city?: string) {',
|
|
438
|
+
" return useObjects('Shipment', {",
|
|
439
|
+
' where: delayedWhere(city),',
|
|
440
|
+
" orderBy: [{ property: 'promisedAt', dir: 'asc' }],",
|
|
441
|
+
' pageToken,',
|
|
442
|
+
' pageSize: PAGE_SIZE,',
|
|
443
|
+
' placeholderData: keepPreviousData,',
|
|
444
|
+
' })',
|
|
445
|
+
'}',
|
|
446
|
+
'',
|
|
447
|
+
'export function useDelayedShipmentTotal(city?: string) {',
|
|
448
|
+
' return useAggregate({',
|
|
449
|
+
" objectSet: objectsOf('Shipment', delayedWhere(city)),",
|
|
450
|
+
" aggregations: [{ alias: 'total', fn: 'count' }],",
|
|
451
|
+
' groupBy: [],',
|
|
452
|
+
' })',
|
|
453
|
+
'}',
|
|
454
|
+
'```',
|
|
455
|
+
'',
|
|
456
|
+
'## Naming',
|
|
457
|
+
'',
|
|
458
|
+
'Folder = domain, singular for a standalone entity (`shipment`), plural when the entity is',
|
|
459
|
+
'inherently collective (`api-keys`). Hooks read as questions: `useDelayedShipments`,',
|
|
460
|
+
'`useShipment`, `useDelayedShipmentTotal`. Filter builders are `<thing>Where`. Constants are',
|
|
461
|
+
'SCREAMING_SNAKE only for tuning values such as `PAGE_SIZE`.',
|
|
462
|
+
'',
|
|
463
|
+
'## Red Flags — STOP',
|
|
464
|
+
'',
|
|
465
|
+
'- `rows.filter(...)` or `rows.sort(...)` on data that came back paged.',
|
|
466
|
+
'- `rows.length` presented as a count.',
|
|
467
|
+
'- A raw `fetch()` to the platform, or a URL built by hand.',
|
|
468
|
+
'- A read inside a component, or a `useEffect` that fetches.',
|
|
469
|
+
'- An object type spelled from memory instead of from `frontera blueprint list`.',
|
|
470
|
+
'- Editing `src/generated/frontera-blueprint.ts`.',
|
|
471
|
+
'- A filter used by the rows query but not by the total.',
|
|
472
|
+
'',
|
|
473
|
+
'## Verification',
|
|
474
|
+
'',
|
|
475
|
+
'```bash',
|
|
476
|
+
'bun run typecheck',
|
|
477
|
+
'```',
|
|
478
|
+
'',
|
|
479
|
+
'```bash',
|
|
480
|
+
'frontera app dev',
|
|
481
|
+
'```',
|
|
482
|
+
'',
|
|
483
|
+
'Compare the number under the table with the row count for the same filter in the platform. If',
|
|
484
|
+
'they disagree, the filtering is happening in the wrong place.',
|
|
485
|
+
'',
|
|
486
|
+
'## Scope',
|
|
487
|
+
'',
|
|
488
|
+
'Applies to every Frontera App. Before `bun run blueprint:types` has ever run, object names are',
|
|
489
|
+
'plain strings and rows are untyped — which is why the scaffold starter takes its object type as',
|
|
490
|
+
'an argument. Real features do not.',
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const actions: Skill = {
|
|
495
|
+
name: 'frontera-blueprint-actions',
|
|
496
|
+
description:
|
|
497
|
+
'Use when a Frontera App CHANGES data — submitting a Blueprint Action, rendering only the actions a user may invoke, following a request to its outcome, approvals, and optimistic-concurrency versions. Covers useActions, useSubmitAction, useActionRequest and why a successful submit is not a successful write.',
|
|
498
|
+
body: [
|
|
499
|
+
'# Blueprint Actions',
|
|
500
|
+
'',
|
|
501
|
+
'Actions are the ONLY write path. There is no direct object mutation, and no service route to',
|
|
502
|
+
'reach around one. An Action is governed: who may invoke it, whether it needs approval, and what',
|
|
503
|
+
'it did are decided and recorded server-side.',
|
|
504
|
+
'',
|
|
505
|
+
'```tsx',
|
|
506
|
+
"import { useActions, useAction, useSubmitAction, useActionRequest } from '@frontera-sdk/blueprint/action-hooks'",
|
|
507
|
+
"import { actionEffectOf, recordVersionOf } from '@frontera-sdk/blueprint/action-types'",
|
|
508
|
+
'```',
|
|
509
|
+
'',
|
|
510
|
+
'## Rules',
|
|
511
|
+
'',
|
|
512
|
+
'- **Render controls from `useActions()`**, or `useAction(apiName)` for one — never from a',
|
|
513
|
+
' hard-coded list. The same page offers different actions to different colleagues, and only the',
|
|
514
|
+
' server knows which. A button that 403s on click is worse than one that was never drawn.',
|
|
515
|
+
'- An empty discovery list in development is almost always permissions, not a bug: an Action is',
|
|
516
|
+
' hidden unless it is published, deployed, AND its invoke capability is held by your role.',
|
|
517
|
+
'- **A submit is not a write.** `useSubmitAction(action)` resolves when the Request is RECORDED;',
|
|
518
|
+
' dispatch happens on a background worker. A success toast in `onSuccess` claims something the',
|
|
519
|
+
' App does not know yet.',
|
|
520
|
+
'- Follow the returned id with `useActionRequest(requestId)`. It polls while the lifecycle is',
|
|
521
|
+
' non-terminal and stops once it settles — do not hand-roll `setInterval`.',
|
|
522
|
+
'- Decide what to SAY with `actionEffectOf(request)`: `pending`, `applied`, `refused`, or',
|
|
523
|
+
' `uncertain`. `uncertain` means the outcome could not be established — rendering it as failure',
|
|
524
|
+
' invites a duplicate submit, rendering it as success is a lie. Say it is being checked.',
|
|
525
|
+
'- Approval is a mode, not an afterthought. When `action.approval.mode === "required"`, submitting',
|
|
526
|
+
' creates a Request someone else decides; the reviewer path is `useDecideActionRequest()`, which',
|
|
527
|
+
' takes `{ requestId, decision, reason }`. Do not design a flow that assumes instant application.',
|
|
528
|
+
'- Editing an existing record: pass `expectedVersion: recordVersionOf(instance.data)`, read from',
|
|
529
|
+
' `useObjectInstance` — a LIST row does not carry the version. Omitting it does not fail; the',
|
|
530
|
+
' compare-and-set is skipped and two people overwrite each other with no refusal and no evidence.',
|
|
531
|
+
'- Idempotency keys are minted per `mutate` call. That is the correct lifetime — a retried network',
|
|
532
|
+
' call cannot double-apply, a second click is a second intent — so do not supply your own.',
|
|
533
|
+
'- Refresh reads when the request reaches a terminal APPLIED state, not when the submit resolves.',
|
|
534
|
+
'- `inputSchema` on the descriptor is the contract, and the server validates against it. Mirror',
|
|
535
|
+
' required fields in the form for a good experience; never treat client validation as the gate.',
|
|
536
|
+
'',
|
|
537
|
+
'```tsx',
|
|
538
|
+
'// src/ui/approve-loan/approve-loan.tsx',
|
|
539
|
+
"const { action } = useAction('ApproveLoan')",
|
|
540
|
+
'const submit = useSubmitAction(action)',
|
|
541
|
+
'const [requestId, setRequestId] = useState<string | null>(null)',
|
|
542
|
+
'const request = useActionRequest(requestId)',
|
|
543
|
+
'const effect = actionEffectOf(request.data)',
|
|
544
|
+
'',
|
|
545
|
+
'// Not drawn at all when the caller may not invoke it.',
|
|
546
|
+
'if (!action) return null',
|
|
547
|
+
'',
|
|
548
|
+
'return (',
|
|
549
|
+
' <>',
|
|
550
|
+
' <Button',
|
|
551
|
+
' disabled={submit.isPending || effect === "pending"}',
|
|
552
|
+
' onClick={() =>',
|
|
553
|
+
' submit.mutate(',
|
|
554
|
+
' { objectId: loanId, input: { decision: "approve" }, expectedVersion: recordVersionOf(loan.data) },',
|
|
555
|
+
' { onSuccess: (created) => setRequestId(created.id) },',
|
|
556
|
+
' )',
|
|
557
|
+
' }',
|
|
558
|
+
' >',
|
|
559
|
+
' {copy.loan.approve}',
|
|
560
|
+
' </Button>',
|
|
561
|
+
' {effect === "pending" && <p>{copy.loan.submitted}</p>}',
|
|
562
|
+
' {effect === "applied" && <p>{copy.loan.applied}</p>}',
|
|
563
|
+
' {effect === "refused" && <p role="alert">{copy.loan.refused}</p>}',
|
|
564
|
+
' {effect === "uncertain" && <p role="alert">{copy.loan.checking}</p>}',
|
|
565
|
+
' </>',
|
|
566
|
+
')',
|
|
567
|
+
'```',
|
|
568
|
+
'',
|
|
569
|
+
'## Naming',
|
|
570
|
+
'',
|
|
571
|
+
'Hooks keep the SDK names. A feature that submits one Action is a folder named after the action',
|
|
572
|
+
'in kebab-case (`approve-loan/`), and the request-following state stays in the entry component,',
|
|
573
|
+
'not in the button.',
|
|
574
|
+
'',
|
|
575
|
+
'## Red Flags — STOP',
|
|
576
|
+
'',
|
|
577
|
+
'- A hard-coded action button, or one rendered before `useAction` returns a descriptor.',
|
|
578
|
+
'- Success shown when the submit resolves.',
|
|
579
|
+
'- `setInterval` polling a request.',
|
|
580
|
+
'- `uncertain` collapsed into success or failure.',
|
|
581
|
+
'- An edit submitted with no `expectedVersion`, or a version read from a list row.',
|
|
582
|
+
'- Reads invalidated on submit rather than on a terminal applied state.',
|
|
583
|
+
'- Any write attempted outside an Action.',
|
|
584
|
+
'',
|
|
585
|
+
'## Verification',
|
|
586
|
+
'',
|
|
587
|
+
'```bash',
|
|
588
|
+
'bun run check',
|
|
589
|
+
'```',
|
|
590
|
+
'',
|
|
591
|
+
'```bash',
|
|
592
|
+
'frontera app dev',
|
|
593
|
+
'```',
|
|
594
|
+
'',
|
|
595
|
+
'Submit one for real and watch it to a terminal state. A flow only tested to the submit call has',
|
|
596
|
+
'not been tested — that is the half that always works.',
|
|
597
|
+
'',
|
|
598
|
+
'## Scope',
|
|
599
|
+
'',
|
|
600
|
+
'Applies to every Frontera App that changes data. Authoring the Actions themselves — defining,',
|
|
601
|
+
'binding and deploying them — is CLI and platform work, not App work.',
|
|
602
|
+
],
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const ui: Skill = {
|
|
606
|
+
name: 'frontera-app-ui',
|
|
607
|
+
description:
|
|
608
|
+
'Use when creating, splitting, or refactoring a feature under src/ui/ in a Frontera App — the feature folder layout, one component per file, where hooks and helpers go, and designing one capability for both the embedded frame and a standalone page.',
|
|
609
|
+
body: [
|
|
610
|
+
'# App UI',
|
|
611
|
+
'',
|
|
612
|
+
'Every feature is a folder under `src/ui/`. The layout is not a suggestion: it is what makes a',
|
|
613
|
+
'feature navigable by someone who has never opened it.',
|
|
614
|
+
'',
|
|
615
|
+
'## Canonical layout',
|
|
616
|
+
'',
|
|
617
|
+
'```text',
|
|
618
|
+
'src/ui/<feature>/',
|
|
619
|
+
' <feature>.tsx entry — consumers import THIS file explicitly',
|
|
620
|
+
' components/ sub-components, one per file, kebab-case',
|
|
621
|
+
' <part>.tsx',
|
|
622
|
+
' hooks/ hooks used only by this feature',
|
|
623
|
+
' use-<name>.ts',
|
|
624
|
+
' utils.ts pure helpers shared inside the folder',
|
|
625
|
+
' types.ts types shared inside the folder or with consumers',
|
|
626
|
+
'```',
|
|
627
|
+
'',
|
|
628
|
+
'## Rules',
|
|
629
|
+
'',
|
|
630
|
+
'- **Folder name = entry file stem = kebab-case of the exported component.** `ObjectExplorer`',
|
|
631
|
+
' lives in `object-explorer/object-explorer.tsx`. Do not shorten the folder.',
|
|
632
|
+
'- **One React component per file** under `src/ui/`. Even a single sub-component goes in',
|
|
633
|
+
' `components/`, not next to the entry file.',
|
|
634
|
+
'- Promote a flat file to a folder the moment any of these happen: a second component, a hook',
|
|
635
|
+
' over ~15 lines, a helper used twice, or a type another module imports.',
|
|
636
|
+
'- **No barrels.** Import the explicit path: `import { ObjectExplorer } from "@/ui/object-explorer/object-explorer"`.',
|
|
637
|
+
'- The entry component owns view state and calls the data layer. Sub-components receive data as',
|
|
638
|
+
' props and fetch nothing — that is what keeps a filter change from becoming a request inside a cell.',
|
|
639
|
+
'- Every user-visible string comes from `src/lib/copy.ts`, including empty and error text.',
|
|
640
|
+
'- Design one capability for two containers, not two designs:',
|
|
641
|
+
' - embedded — constrained width and height, platform chrome outside the App, the App owns its',
|
|
642
|
+
' own internal scrolling;',
|
|
643
|
+
' - standalone — full viewport, direct navigation, room for denser controls.',
|
|
644
|
+
'- Mobile-first: fluid layouts, no fixed viewport widths, primary controls at least 44px tall,',
|
|
645
|
+
' wide content scrolling inside its own container so the page never scrolls sideways.',
|
|
646
|
+
'- Never hide a workflow on small screens with no replacement. Collapse a secondary rail into a',
|
|
647
|
+
' disclosure, do not amputate it.',
|
|
648
|
+
'- Colors, spacing and elevation come from tokens — see **frontera-design-tokens**.',
|
|
649
|
+
'- Shared primitives come from the shadcn registry (`bunx --bun shadcn@latest add <name>`), not',
|
|
650
|
+
' from hand-written markup — see **frontera-ui-components**.',
|
|
651
|
+
'',
|
|
652
|
+
'```tsx',
|
|
653
|
+
'// src/ui/object-explorer/object-explorer.tsx — the entry owns state, children take props',
|
|
654
|
+
'const rows = useObjectPage(params)',
|
|
655
|
+
'return (',
|
|
656
|
+
' <section className="flex flex-col gap-4">',
|
|
657
|
+
' <ObjectSearch value={search} onChange={handleSearch} />',
|
|
658
|
+
' <ObjectTable columns={columns} rows={records} isRefreshing={rows.isFetching} />',
|
|
659
|
+
' </section>',
|
|
660
|
+
')',
|
|
661
|
+
'```',
|
|
662
|
+
'',
|
|
663
|
+
'## Naming',
|
|
664
|
+
'',
|
|
665
|
+
'Filenames kebab-case, always. Components PascalCase, hooks `use<Name>`, helpers camelCase.',
|
|
666
|
+
'Skeletons are `<part>-skeleton.tsx` next to the part they replace.',
|
|
667
|
+
'',
|
|
668
|
+
'## Red Flags — STOP',
|
|
669
|
+
'',
|
|
670
|
+
'- A sub-component at the folder root instead of in `components/`.',
|
|
671
|
+
'- An `index.ts` created "just for re-exports".',
|
|
672
|
+
'- A PascalCase filename.',
|
|
673
|
+
'- A component that calls a Blueprint hook and also renders a leaf of the tree.',
|
|
674
|
+
'- Hardcoded strings instead of `copy`.',
|
|
675
|
+
'- A layout that assumes the App owns the whole viewport.',
|
|
676
|
+
'- Hover-only affordances, or icon-only controls with no accessible name.',
|
|
677
|
+
'',
|
|
678
|
+
'## Verification',
|
|
679
|
+
'',
|
|
680
|
+
'```bash',
|
|
681
|
+
'bun run check',
|
|
682
|
+
'```',
|
|
683
|
+
'',
|
|
684
|
+
'Then open the App at a narrow width and confirm nothing overflows the page horizontally.',
|
|
685
|
+
'',
|
|
686
|
+
'## Scope',
|
|
687
|
+
'',
|
|
688
|
+
'Applies to everything under `src/ui/`. `src/components/ui/` follows the primitive conventions',
|
|
689
|
+
'instead — see **frontera-ui-components**.',
|
|
690
|
+
],
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const tokens: Skill = {
|
|
694
|
+
name: 'frontera-design-tokens',
|
|
695
|
+
description:
|
|
696
|
+
'Use when styling any Frontera App surface, choosing colors or elevation, or adding a CSS variable — the token utilities in src/app/globals.css, host-driven dark mode, and why a hex literal in a component breaks customer branding.',
|
|
697
|
+
body: [
|
|
698
|
+
'# Design tokens',
|
|
699
|
+
'',
|
|
700
|
+
'Tailwind v4, CSS-first. `src/app/globals.css` declares raw variables and re-exports them as',
|
|
701
|
+
'utilities through `@theme inline`. There is no `tailwind.config.*`.',
|
|
702
|
+
'',
|
|
703
|
+
'## Rules',
|
|
704
|
+
'',
|
|
705
|
+
'- Reference tokens through utility classes: `bg-background`, `text-foreground`, `bg-card`,',
|
|
706
|
+
' `text-muted-foreground`, `border-border`, `text-destructive`, `ring-ring`, `rounded-lg`.',
|
|
707
|
+
'- Pick a surface by stacking depth, not by eye: `surface-inset-deep` < `surface-inset` <',
|
|
708
|
+
' `surface` < `surface-raised` < `surface-overlay`, each with `-hover` and `-active` variants.',
|
|
709
|
+
'- **Never write a raw color in a component** — no `#…`, no `oklch(…)`, no `hsl(…)`. The host',
|
|
710
|
+
' writes its palette into these variables at handshake, and a customer brand overrides them.',
|
|
711
|
+
' A literal is the one thing branding cannot reach.',
|
|
712
|
+
'- Dark mode is a class the SDK toggles on `<html>` from the host theme. Style it with the `dark:`',
|
|
713
|
+
' variant; never write the selector by hand, and never force a dark shell.',
|
|
714
|
+
'- Both themes are first class. If a surface only looks right in one of them, it is not done.',
|
|
715
|
+
'- To add a token, edit BOTH the raw declaration (light and dark) AND the `@theme inline` mirror,',
|
|
716
|
+
' or the utility will not exist.',
|
|
717
|
+
'- `chart-*` colors are for data visualization only, never for UI chrome.',
|
|
718
|
+
'- Merge classes through `cn()` from `@/lib/utils`, with the caller className LAST.',
|
|
719
|
+
'',
|
|
720
|
+
'```tsx',
|
|
721
|
+
"import { cn } from '@/lib/utils'",
|
|
722
|
+
'',
|
|
723
|
+
'<div',
|
|
724
|
+
' className={cn(',
|
|
725
|
+
" 'rounded-lg border border-border bg-card p-4 text-sm text-foreground',",
|
|
726
|
+
" isRefreshing && 'opacity-60',",
|
|
727
|
+
' className,',
|
|
728
|
+
' )}',
|
|
729
|
+
'/>',
|
|
730
|
+
'```',
|
|
731
|
+
'',
|
|
732
|
+
'## Naming',
|
|
733
|
+
'',
|
|
734
|
+
'Variables are `--color-*`, `--surface-*`, `--radius-*`, `--chart-N`; state suffixes are',
|
|
735
|
+
'`-hover` and `-active`. Utilities read the same way with the prefix dropped.',
|
|
736
|
+
'',
|
|
737
|
+
'## Red Flags — STOP',
|
|
738
|
+
'',
|
|
739
|
+
'- A color literal anywhere under `src/ui/` or `src/components/`.',
|
|
740
|
+
'- A new raw variable with no `@theme inline` mirror.',
|
|
741
|
+
'- Writing the dark selector directly in a component.',
|
|
742
|
+
'- A surface that is unreadable in the theme you did not test.',
|
|
743
|
+
'- `chart-*` used for chrome.',
|
|
744
|
+
'- A locally redefined `cn`.',
|
|
745
|
+
'',
|
|
746
|
+
'## Verification',
|
|
747
|
+
'',
|
|
748
|
+
'```bash',
|
|
749
|
+
'bun run build',
|
|
750
|
+
'```',
|
|
751
|
+
'',
|
|
752
|
+
'Tailwind fails the build on an unknown utility. Then check the App in both themes — the host',
|
|
753
|
+
'toggle in the platform, or the system setting standalone.',
|
|
754
|
+
'',
|
|
755
|
+
'## Scope',
|
|
756
|
+
'',
|
|
757
|
+
'The token names ship with the scaffold stylesheet, which is a projection of the platform',
|
|
758
|
+
'stylesheet. A customer deployment may redefine their values; it does not rename them.',
|
|
759
|
+
],
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const components: Skill = {
|
|
763
|
+
name: 'frontera-ui-components',
|
|
764
|
+
description:
|
|
765
|
+
'Use when adding or editing a shared primitive in a Frontera App — pulling components from the shadcn registry with bunx shadcn add, why shadcn init must never run, and the cva + data-slot + cn conventions for editing one.',
|
|
766
|
+
body: [
|
|
767
|
+
'# Shared components',
|
|
768
|
+
'',
|
|
769
|
+
'Primitives come from the **shadcn registry**. This project does not maintain a component',
|
|
770
|
+
'library: `components.json` is already configured, so any upstream component drops in unmodified.',
|
|
771
|
+
'Once added, the file is copied source and yours to edit.',
|
|
772
|
+
'',
|
|
773
|
+
'## Rules',
|
|
774
|
+
'',
|
|
775
|
+
'- Add what you need, by name, before writing one yourself:',
|
|
776
|
+
'',
|
|
777
|
+
'```bash',
|
|
778
|
+
'bunx --bun shadcn@latest add dialog dropdown-menu tabs',
|
|
779
|
+
'```',
|
|
780
|
+
'',
|
|
781
|
+
' Files land in `src/components/ui/`, importing `cn` from `@/lib/utils`. The scaffold already',
|
|
782
|
+
' ships `button`, `input`, `table`, `skeleton`, `card` and `alert` — check there first.',
|
|
783
|
+
'- **Never run `shadcn init`.** It rewrites `src/app/globals.css` with the vanilla palette and',
|
|
784
|
+
' drops the platform surface scale, so every screen in the App changes colour at once. `add`',
|
|
785
|
+
' leaves that stylesheet alone, which is the whole reason this configuration exists.',
|
|
786
|
+
'- The registry declares no npm dependencies of its own, so an added component installs nothing.',
|
|
787
|
+
' Everything upstream imports — `class-variance-authority`, `radix-ui`, `lucide-react`,',
|
|
788
|
+
' `tw-animate-css` — is already in `package.json`. If a new component wants something else,',
|
|
789
|
+
' add it with `bun add` and say so in the same commit.',
|
|
790
|
+
'- A primitive is shared, generic, and knows nothing about your data. Anything that knows about',
|
|
791
|
+
' one feature belongs in `src/ui/<feature>/components/` instead.',
|
|
792
|
+
'- `src/components/ui/` is EXEMPT from one-component-per-file: compound primitives keep their',
|
|
793
|
+
' parts together (`Card`, `CardHeader`, `CardContent`).',
|
|
794
|
+
'- Editing one is normal — it is your file. Keep the conventions it arrived with: variants',
|
|
795
|
+
' through `cva` with `defaultVariants`, props typed as',
|
|
796
|
+
' `React.ComponentProps<"el"> & VariantProps<typeof xVariants>`, classes merged with `cn()` with',
|
|
797
|
+
' the caller className LAST, a kebab `data-slot` on every part, and both the component and its',
|
|
798
|
+
' variants function exported.',
|
|
799
|
+
'- Colours come from tokens — see **frontera-design-tokens**. Upstream components reference the',
|
|
800
|
+
' same token names this stylesheet defines, which is why they look native without patching.',
|
|
801
|
+
'- `frontera app add` is a second, Frontera-specific registry: page and dashboard compositions',
|
|
802
|
+
' (`app-page`, `page-header`, `stat-tile`, `filter-bar`, `data-table`) that upstream has no',
|
|
803
|
+
' equivalent for. Use it for those; use shadcn for everything generic.',
|
|
804
|
+
'',
|
|
805
|
+
'```tsx',
|
|
806
|
+
'// src/components/ui/badge.tsx — as shadcn writes it, and as you keep it',
|
|
807
|
+
'function Badge({ className, variant, ...props }:',
|
|
808
|
+
" React.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {",
|
|
809
|
+
' return <span data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />',
|
|
810
|
+
'}',
|
|
811
|
+
'',
|
|
812
|
+
'export { Badge, badgeVariants }',
|
|
813
|
+
'```',
|
|
814
|
+
'',
|
|
815
|
+
'## Naming',
|
|
816
|
+
'',
|
|
817
|
+
'Files kebab-case, as the registry writes them. Exports PascalCase with compound sub-exports.',
|
|
818
|
+
'The variants function is `<name>Variants`; variant values are lowercase.',
|
|
819
|
+
'',
|
|
820
|
+
'## Red Flags — STOP',
|
|
821
|
+
'',
|
|
822
|
+
'- Hand-writing a button, input, dialog or table that the registry publishes.',
|
|
823
|
+
'- Running `shadcn init` in an existing project.',
|
|
824
|
+
'- Re-adding a component over local edits without meaning to.',
|
|
825
|
+
'- Feature-specific logic inside `src/components/ui/`.',
|
|
826
|
+
'- A missing `data-slot`, a locally redefined `cn`, or a variant system invented next to `cva`.',
|
|
827
|
+
'- Editing a token to make one component look right — that changes every component.',
|
|
828
|
+
'',
|
|
829
|
+
'## Verification',
|
|
830
|
+
'',
|
|
831
|
+
'```bash',
|
|
832
|
+
'bun run check',
|
|
833
|
+
'```',
|
|
834
|
+
'',
|
|
835
|
+
'Then look at the component in both themes. A primitive that only works in one of them is not done.',
|
|
836
|
+
'',
|
|
837
|
+
'## Scope',
|
|
838
|
+
'',
|
|
839
|
+
'The shadcn registry changes independently of this project — list or search it rather than',
|
|
840
|
+
'assuming this skill knows what is in it. A component that needs a Radix package the project',
|
|
841
|
+
'does not have will say so at build time.',
|
|
842
|
+
],
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const queryStates: Skill = {
|
|
846
|
+
name: 'frontera-query-ui-states',
|
|
847
|
+
description:
|
|
848
|
+
'Use when rendering asynchronous Blueprint-backed UI in a Frontera App — the error/loading/empty/content branch order, content-shaped skeletons, background refetch dimming, and telling forbidden apart from empty.',
|
|
849
|
+
body: [
|
|
850
|
+
'# Query states',
|
|
851
|
+
'',
|
|
852
|
+
'A query has more outcomes than "data" and "not yet". Rendering them in a deliberate order is',
|
|
853
|
+
'what stops an App from telling a customer their records are gone when a request merely failed.',
|
|
854
|
+
'',
|
|
855
|
+
'## Rules',
|
|
856
|
+
'',
|
|
857
|
+
'- Branch in this order: **error → initial loading → empty → content**. Configuration gaps come',
|
|
858
|
+
' first of all, because nothing below them can succeed.',
|
|
859
|
+
'- `isLoading` is the FIRST load and earns a skeleton. `isFetching` is a background refresh: keep',
|
|
860
|
+
' the stale rows, dim them, mark the region `aria-busy`, and never reset selection or scroll.',
|
|
861
|
+
'- A skeleton is shaped like the content it replaces — the same rows, the same column count, the',
|
|
862
|
+
' same border. A centred spinner communicates nothing and moves the layout when it disappears.',
|
|
863
|
+
'- Keep the skeleton in its own file next to the component: `<part>-skeleton.tsx`.',
|
|
864
|
+
'- Default optional data only AFTER error and loading have been distinguished. `data?.rows ?? []`',
|
|
865
|
+
' is a rendering convenience, never a substitute for handling a failure.',
|
|
866
|
+
'- An empty success says what was queried and offers the next useful action. A forbidden result',
|
|
867
|
+
' says access is unavailable — it must not read as "there are no records".',
|
|
868
|
+
'- A recoverable error keeps the surrounding context, uses `role="alert"`, and offers a retry that',
|
|
869
|
+
' calls `refetch`.',
|
|
870
|
+
'- Every one of those strings comes from `src/lib/copy.ts`.',
|
|
871
|
+
'',
|
|
872
|
+
'```tsx',
|
|
873
|
+
'// src/ui/object-explorer/object-explorer.tsx',
|
|
874
|
+
'const records = rows.data?.rows ?? []',
|
|
875
|
+
'',
|
|
876
|
+
'return rows.isError ? (',
|
|
877
|
+
' <QueryError message={rows.error.message} onRetry={() => void rows.refetch()} />',
|
|
878
|
+
') : rows.isLoading ? (',
|
|
879
|
+
' <ObjectTableSkeleton />',
|
|
880
|
+
') : records.length === 0 ? (',
|
|
881
|
+
' <EmptyPanel title={copy.explorer.empty} body={copy.explorer.emptyHint} />',
|
|
882
|
+
') : (',
|
|
883
|
+
' <ObjectTable columns={columns} rows={records} isRefreshing={rows.isFetching} />',
|
|
884
|
+
')',
|
|
885
|
+
'```',
|
|
886
|
+
'',
|
|
887
|
+
'## Naming',
|
|
888
|
+
'',
|
|
889
|
+
'Query flags stay as React Query names them: `isLoading`, `isFetching`, `isError`. Shared state',
|
|
890
|
+
'components are `empty-panel.tsx`, `query-error.tsx`, `<part>-skeleton.tsx`.',
|
|
891
|
+
'',
|
|
892
|
+
'## Red Flags — STOP',
|
|
893
|
+
'',
|
|
894
|
+
'- An unhandled `isError`.',
|
|
895
|
+
'- A bare spinner where a shaped skeleton belongs.',
|
|
896
|
+
'- A failed read rendered as an empty state.',
|
|
897
|
+
'- The table remounting on every page change instead of dimming.',
|
|
898
|
+
'- Empty or error copy written inline.',
|
|
899
|
+
'',
|
|
900
|
+
'## Verification',
|
|
901
|
+
'',
|
|
902
|
+
'```bash',
|
|
903
|
+
'frontera app dev',
|
|
904
|
+
'```',
|
|
905
|
+
'',
|
|
906
|
+
'Exercise all four states: a filter that matches nothing, a stopped network, a first load, and a',
|
|
907
|
+
'page change. Each must look different.',
|
|
908
|
+
'',
|
|
909
|
+
'## Scope',
|
|
910
|
+
'',
|
|
911
|
+
'Applies to every query consumer in a Frontera App.',
|
|
912
|
+
],
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const tables: Skill = {
|
|
916
|
+
name: 'frontera-app-tables',
|
|
917
|
+
description:
|
|
918
|
+
'Use when rendering Blueprint rows as a table or list in a Frontera App — server-driven paging and sorting, totals from an aggregate, column definitions, the registry data-table, and the narrow-screen strategy.',
|
|
919
|
+
body: [
|
|
920
|
+
'# Tables',
|
|
921
|
+
'',
|
|
922
|
+
'A table over Blueprint is a server-driven table. The dataset is larger than the page, always.',
|
|
923
|
+
'',
|
|
924
|
+
'## Rules',
|
|
925
|
+
'',
|
|
926
|
+
'- Start from `src/ui/object-explorer/` — search, shaped skeleton, dimmed refetch, pager and',
|
|
927
|
+
' aggregate total are already wired there over the shadcn `table` primitive.',
|
|
928
|
+
'- For sorting, faceted filters, column visibility and a toolbar, do NOT hand-roll them. The',
|
|
929
|
+
' shadcn registry has no data-table item — it is a documentation example — so take the',
|
|
930
|
+
' Frontera one, which is that example already wired to this token system:',
|
|
931
|
+
'',
|
|
932
|
+
'```bash',
|
|
933
|
+
'frontera app add data-table',
|
|
934
|
+
'```',
|
|
935
|
+
'',
|
|
936
|
+
' It brings `@tanstack/react-table`; install what it reports as missing.',
|
|
937
|
+
'',
|
|
938
|
+
'- Declare columns in their own file, `<feature>-columns.tsx`, exporting a FUNCTION',
|
|
939
|
+
' (`getShipmentColumns(options)`) so values discovered at runtime can be injected. Never rebuild',
|
|
940
|
+
' the column list inline on every render.',
|
|
941
|
+
'- Everything that changes the result set goes to the server: `where`, `orderBy`, `pageToken`,',
|
|
942
|
+
' `pageSize`. Client-side sorting of a page reorders 25 rows and calls it a ranking.',
|
|
943
|
+
'- The first request omits `pageToken`. Advance with the response `nextPageToken`; retain earlier',
|
|
944
|
+
' tokens locally if the UI supports Previous. Never calculate, inspect or manufacture a token.',
|
|
945
|
+
'- Use `hasMore` to enable Next. An aggregate total is useful for a displayed range such as',
|
|
946
|
+
' "1–25 of 8,961", but it does not drive Blueprint navigation.',
|
|
947
|
+
'- Reset the token history on ANY change to search, filters or sort — do it in the handler, not',
|
|
948
|
+
' in an effect. A cursor belongs to the exact query shape that created it.',
|
|
949
|
+
'- Debounce free-text search around 300ms; every keystroke otherwise becomes a Blueprint scan.',
|
|
950
|
+
'- Keep the skeleton column count in step with the real column count.',
|
|
951
|
+
'- Narrow screens need a strategy, not a shrug: scroll the table inside its own',
|
|
952
|
+
' `overflow-x-auto` container, or render a card list. The page itself must never scroll sideways.',
|
|
953
|
+
'',
|
|
954
|
+
'```tsx',
|
|
955
|
+
'// reset cursor history where the query shape changes',
|
|
956
|
+
'const handleSearch = (value: string) => {',
|
|
957
|
+
' setSearch(value)',
|
|
958
|
+
' setPageIndex(0)',
|
|
959
|
+
' setPageTokens([undefined])',
|
|
960
|
+
'}',
|
|
961
|
+
'',
|
|
962
|
+
'<ObjectPager',
|
|
963
|
+
' hasPrevious={pageIndex > 0}',
|
|
964
|
+
' hasNext={rows.data?.hasMore ?? false}',
|
|
965
|
+
' onPrevious={() => setPageIndex((index) => Math.max(0, index - 1))}',
|
|
966
|
+
' onNext={() => {',
|
|
967
|
+
' const token = rows.data?.nextPageToken',
|
|
968
|
+
' if (!token) return',
|
|
969
|
+
' setPageTokens((tokens) => [...tokens.slice(0, pageIndex + 1), token])',
|
|
970
|
+
' setPageIndex((index) => index + 1)',
|
|
971
|
+
' }}',
|
|
972
|
+
'/>',
|
|
973
|
+
'```',
|
|
974
|
+
'',
|
|
975
|
+
'## Naming',
|
|
976
|
+
'',
|
|
977
|
+
'Table component `<feature>-table.tsx`, columns `<feature>-columns.tsx` exporting',
|
|
978
|
+
'`get<Feature>Columns()`, skeleton `<feature>-table-skeleton.tsx`, pager `<feature>-pager.tsx`.',
|
|
979
|
+
'',
|
|
980
|
+
'## Red Flags — STOP',
|
|
981
|
+
'',
|
|
982
|
+
'- Sorting or filtering an array that came from a paged query.',
|
|
983
|
+
'- `rows.length` used as the total, or a footer that says "Page 1 of 1" over a filtered set.',
|
|
984
|
+
'- Sending a numeric page or offset to Blueprint instead of using `nextPageToken`.',
|
|
985
|
+
'- Reusing a cursor after its filter, sort or projection changes.',
|
|
986
|
+
'- Columns rebuilt inline, or a second table implementation next to the first.',
|
|
987
|
+
'- A table that forces the whole page to scroll horizontally on a phone.',
|
|
988
|
+
'',
|
|
989
|
+
'## Verification',
|
|
990
|
+
'',
|
|
991
|
+
'```bash',
|
|
992
|
+
'bun run check',
|
|
993
|
+
'```',
|
|
994
|
+
'',
|
|
995
|
+
'Then, with real data: filter to a subset, confirm the total matches the filter, page to the end,',
|
|
996
|
+
'and check the layout at a phone width.',
|
|
997
|
+
'',
|
|
998
|
+
'## Scope',
|
|
999
|
+
'',
|
|
1000
|
+
'Applies to every list surface in a Frontera App, including card lists and boards — the paging',
|
|
1001
|
+
'and total rules are about the query, not the markup.',
|
|
1002
|
+
],
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const testing: Skill = {
|
|
1006
|
+
name: 'frontera-app-testing',
|
|
1007
|
+
description:
|
|
1008
|
+
'Use when adding or running tests in a Frontera App — bun test over pure helpers in src/lib, what is worth testing in a static App, and why data behaviour is verified with frontera app dev rather than mocks.',
|
|
1009
|
+
body: [
|
|
1010
|
+
'# Testing',
|
|
1011
|
+
'',
|
|
1012
|
+
'A static App has a small, sharp testable core: the pure functions that decide what to ask for',
|
|
1013
|
+
'and how to present the answer. Test those, and verify the rest against real data.',
|
|
1014
|
+
'',
|
|
1015
|
+
'## Rules',
|
|
1016
|
+
'',
|
|
1017
|
+
'- `bun run test` runs `bun test src`. Test files are named `*.test.ts` and colocated in',
|
|
1018
|
+
' `__tests__/` next to the code they cover. Never `.spec.ts`.',
|
|
1019
|
+
'- Test the pure layer: filter builders, page arithmetic, totals, formatters, deep-link parsing.',
|
|
1020
|
+
' These are exactly the functions a Blueprint query depends on and a screenshot cannot check.',
|
|
1021
|
+
'- Extract a helper to make it testable rather than testing through a hook. If the logic is',
|
|
1022
|
+
' trapped inside a component, that is a design problem, not a testing problem.',
|
|
1023
|
+
'- Do not mock the Blueprint client to assert hook internals. A mocked read proves the mock:',
|
|
1024
|
+
' it will happily agree that a filter compiles into an object set the service would reject.',
|
|
1025
|
+
'- Verify data behaviour by running the App against a real workspace with `frontera app dev`, and',
|
|
1026
|
+
' confirm the states from **frontera-query-ui-states** each render.',
|
|
1027
|
+
'- No DOM test runner ships with the scaffold. If a component genuinely needs one, add the runner',
|
|
1028
|
+
' and its configuration in the same commit, wire it into the `check` script, and say why.',
|
|
1029
|
+
'- Tests must be deterministic and offline: no network, no clock dependence, no shared state',
|
|
1030
|
+
' between files.',
|
|
1031
|
+
'',
|
|
1032
|
+
'```ts',
|
|
1033
|
+
'// src/lib/blueprint/objects/__tests__/objects-hooks.test.ts',
|
|
1034
|
+
"import { describe, expect, test } from 'bun:test'",
|
|
1035
|
+
"import { objectWhere } from '../objects-hooks'",
|
|
1036
|
+
'',
|
|
1037
|
+
"describe('objectWhere', () => {",
|
|
1038
|
+
" test('ignores whitespace-only input rather than filtering on an empty string', () => {",
|
|
1039
|
+
" expect(objectWhere({ objectType: 'Shipment', search: ' ', searchProperty: 'city' }))",
|
|
1040
|
+
' .toBeUndefined()',
|
|
1041
|
+
' })',
|
|
1042
|
+
'})',
|
|
1043
|
+
'```',
|
|
1044
|
+
'',
|
|
1045
|
+
'## Naming',
|
|
1046
|
+
'',
|
|
1047
|
+
'`describe("<unit>")` names the function; `test("<verb phrase>")` states the behaviour and the',
|
|
1048
|
+
'consequence, not the mechanics. Fixtures are factory functions, not shared mutable objects.',
|
|
1049
|
+
'',
|
|
1050
|
+
'## Red Flags — STOP',
|
|
1051
|
+
'',
|
|
1052
|
+
'- `.spec.ts` naming.',
|
|
1053
|
+
'- A test that asserts a mock was called.',
|
|
1054
|
+
'- Logic left inside a component because it "cannot be tested".',
|
|
1055
|
+
'- A test that hits the network, or depends on the current date.',
|
|
1056
|
+
'- Claiming a data change works without having run it against real data.',
|
|
1057
|
+
'',
|
|
1058
|
+
'## Verification',
|
|
1059
|
+
'',
|
|
1060
|
+
'```bash',
|
|
1061
|
+
'bun run test',
|
|
1062
|
+
'```',
|
|
1063
|
+
'',
|
|
1064
|
+
'## Scope',
|
|
1065
|
+
'',
|
|
1066
|
+
'Applies to every Frontera App. The scaffold ships one example suite next to the starter data',
|
|
1067
|
+
'layer; it is the shape to copy, not the extent of what should be tested.',
|
|
1068
|
+
],
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const security: Skill = {
|
|
1072
|
+
name: 'frontera-app-security',
|
|
1073
|
+
description:
|
|
1074
|
+
'Use when adding external requests, browser capabilities, secrets, or iframe behaviour to a Frontera App — declaring CSP domains, why a static App has no secret storage, and the boundaries of the App token.',
|
|
1075
|
+
body: [
|
|
1076
|
+
'# Security boundaries',
|
|
1077
|
+
'',
|
|
1078
|
+
'An App runs in a sandboxed frame with a short-lived, App-scoped credential. Every boundary here',
|
|
1079
|
+
'exists because the App is browser code a customer can read.',
|
|
1080
|
+
'',
|
|
1081
|
+
'## Rules',
|
|
1082
|
+
'',
|
|
1083
|
+
'- Declare every external host in `frontera.config.json`: `connectDomains` for connections the',
|
|
1084
|
+
' browser makes, `resourceDomains` for images, fonts and other resources. The deployed manifest',
|
|
1085
|
+
' becomes CSP, so anything undeclared fails closed — including in the preview you are about to test.',
|
|
1086
|
+
'- **Browser code has no secret storage.** Never put a workspace key, service credential or private',
|
|
1087
|
+
' token in source, in `NEXT_PUBLIC_*`, in `.env*`, in a URL, in `localStorage`, in a log line, or',
|
|
1088
|
+
' in App state. A static App cannot hold a secret; the honest answer to "where do I put the key"',
|
|
1089
|
+
' is that this App does not get one.',
|
|
1090
|
+
'- The App token is short-lived, App-scoped and limited to the allowlisted read surface. Embedded',
|
|
1091
|
+
' and standalone bind it to the signed-in user; local development binds it to the stored CLI',
|
|
1092
|
+
' credential. Treat it as opaque and do not move it anywhere.',
|
|
1093
|
+
'- Do not recreate the iframe bridge, weaken the parent-origin check, accept `*` as a postMessage',
|
|
1094
|
+
' target, or treat an asset cookie as a data credential.',
|
|
1095
|
+
'- Do not request a service route just because the browser can construct the URL. Blueprint is the',
|
|
1096
|
+
' contract; anything outside it is not an entitlement you have.',
|
|
1097
|
+
'- Never send customer data to a third-party host without an explicit decision that the customer',
|
|
1098
|
+
' permits it to leave Frontera. Adding a domain is that decision, in writing.',
|
|
1099
|
+
'- Treat data returned by a query as data. Rendering user-controlled HTML, or building a URL from',
|
|
1100
|
+
' a record without validating its scheme, hands the page to whoever wrote the row.',
|
|
1101
|
+
'',
|
|
1102
|
+
'## Before adding a domain',
|
|
1103
|
+
'',
|
|
1104
|
+
'1. Confirm the browser genuinely needs direct access, rather than the data arriving through Blueprint.',
|
|
1105
|
+
'2. Use HTTPS, name the narrowest host that works, and avoid wildcards.',
|
|
1106
|
+
'3. Put it in the right list — a connection is not a resource.',
|
|
1107
|
+
'4. Redeploy: CSP comes from the deployed manifest, so a local change proves nothing.',
|
|
1108
|
+
'',
|
|
1109
|
+
'## Red Flags — STOP',
|
|
1110
|
+
'',
|
|
1111
|
+
'- A credential in `.env`, in a `NEXT_PUBLIC_*` variable, or in committed source.',
|
|
1112
|
+
'- A wildcard domain, or a domain added to silence a CSP error without understanding it.',
|
|
1113
|
+
'- A hand-rolled postMessage listener next to the SDK bridge.',
|
|
1114
|
+
'- A token logged, cached, stored, or attached to a third-party request.',
|
|
1115
|
+
'- `dangerouslySetInnerHTML` over query results.',
|
|
1116
|
+
'',
|
|
1117
|
+
'## Verification',
|
|
1118
|
+
'',
|
|
1119
|
+
'```bash',
|
|
1120
|
+
'frontera app deploy --no-promote',
|
|
1121
|
+
'```',
|
|
1122
|
+
'',
|
|
1123
|
+
'Open the preview URL and watch the browser console: a blocked request is a CSP declaration you',
|
|
1124
|
+
'have not made yet. Then confirm nothing sensitive appears in the built output.',
|
|
1125
|
+
'',
|
|
1126
|
+
'## Scope',
|
|
1127
|
+
'',
|
|
1128
|
+
'Applies to every Frontera App. Server-side capabilities and Frontera Secrets are out of scope',
|
|
1129
|
+
'here — a static App has no server side to hold them.',
|
|
1130
|
+
],
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const release: Skill = {
|
|
1134
|
+
name: 'frontera-app-release',
|
|
1135
|
+
description:
|
|
1136
|
+
'Use when validating, building, deploying, previewing, promoting, or rolling back a Frontera App — the safe release sequence, version immutability, and what the recorded provenance does and does not prove.',
|
|
1137
|
+
body: [
|
|
1138
|
+
'# Release',
|
|
1139
|
+
'',
|
|
1140
|
+
'Frontera serves an uploaded immutable artifact. It does not install dependencies and does not',
|
|
1141
|
+
'run Next, so whatever `bun run build` produced is exactly what customers get.',
|
|
1142
|
+
'',
|
|
1143
|
+
'## Safe sequence',
|
|
1144
|
+
'',
|
|
1145
|
+
'1. `bun run check` — typecheck, tests, then the build. The build is the authoritative',
|
|
1146
|
+
' static-export compatibility check.',
|
|
1147
|
+
'2. Confirm `out/index.html` exists, along with every route you intended to export.',
|
|
1148
|
+
'3. `frontera app deploy --no-promote` — publishes a version without moving the live pointer.',
|
|
1149
|
+
'4. Open the returned preview URL. Check it embedded and at a narrow width, in both themes, and',
|
|
1150
|
+
' watch the console for blocked requests.',
|
|
1151
|
+
'5. `frontera app promote <version>` — point live at it.',
|
|
1152
|
+
'',
|
|
1153
|
+
'```bash',
|
|
1154
|
+
'frontera app deploy --no-promote',
|
|
1155
|
+
'```',
|
|
1156
|
+
'',
|
|
1157
|
+
'```bash',
|
|
1158
|
+
'frontera app versions',
|
|
1159
|
+
'```',
|
|
1160
|
+
'',
|
|
1161
|
+
'## Rules',
|
|
1162
|
+
'',
|
|
1163
|
+
'- Versions are immutable. Never reuse a version string for different bytes.',
|
|
1164
|
+
'- Rollback is promoting an earlier version, not rebuilding one. That is the whole reason the old',
|
|
1165
|
+
' bytes are kept.',
|
|
1166
|
+
'- Promote deliberately. A deploy that promotes by default turns every experiment into a release.',
|
|
1167
|
+
'- `frontera app versions` shows runtime, routing, framework provenance, source commit and dirty',
|
|
1168
|
+
' status, sizes and the live pointer. The CLI-recorded provenance is metadata; the',
|
|
1169
|
+
' service-computed digest, file count, byte count and uploader are authoritative.',
|
|
1170
|
+
'- Deploying from a dirty tree is recorded as dirty. Do not do it for a release you intend to',
|
|
1171
|
+
' reproduce later.',
|
|
1172
|
+
'- Customer CI runs the same two commands with credentials from the CI secret store, never from',
|
|
1173
|
+
' the repository.',
|
|
1174
|
+
'- A schema-dependent change needs `bun run blueprint:types:check` in CI: the App can build',
|
|
1175
|
+
' perfectly against a contract the workspace no longer has.',
|
|
1176
|
+
'',
|
|
1177
|
+
'## Red Flags — STOP',
|
|
1178
|
+
'',
|
|
1179
|
+
'- Promoting a version nobody opened.',
|
|
1180
|
+
'- Deploying without `bun run check`, or after only a typecheck.',
|
|
1181
|
+
'- Editing `out/` by hand.',
|
|
1182
|
+
'- Rebuilding to "roll back".',
|
|
1183
|
+
'- A credential in CI configuration instead of the secret store.',
|
|
1184
|
+
'',
|
|
1185
|
+
'## Verification',
|
|
1186
|
+
'',
|
|
1187
|
+
'```bash',
|
|
1188
|
+
'frontera app versions',
|
|
1189
|
+
'```',
|
|
1190
|
+
'',
|
|
1191
|
+
'The live pointer must name the version you promoted, and its digest must match the deploy you',
|
|
1192
|
+
'previewed.',
|
|
1193
|
+
'',
|
|
1194
|
+
'## Scope',
|
|
1195
|
+
'',
|
|
1196
|
+
'Applies to every Frontera App. Automations and platform resources have their own lifecycles and',
|
|
1197
|
+
'are not released this way.',
|
|
1198
|
+
],
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const SKILLS: Skill[] = [
|
|
1202
|
+
dispatcher,
|
|
1203
|
+
project,
|
|
1204
|
+
routing,
|
|
1205
|
+
session,
|
|
1206
|
+
blueprintData,
|
|
1207
|
+
actions,
|
|
1208
|
+
ui,
|
|
1209
|
+
tokens,
|
|
1210
|
+
components,
|
|
1211
|
+
queryStates,
|
|
1212
|
+
tables,
|
|
1213
|
+
testing,
|
|
1214
|
+
security,
|
|
1215
|
+
release,
|
|
1216
|
+
]
|