@terreno/agent-plugin 0.0.0-stage → 57.21.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.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2124 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +83 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +280 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +102 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +91 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +156 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +116 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +276 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +45 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +85 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +86 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,116 @@
1
+ # modelRouter — Terreno
2
+
3
+ ## Registration signatures
4
+
5
+ ```typescript
6
+ // TerrenoApp (recommended):
7
+ const todoRouter = modelRouter("/todos", Todo, options);
8
+ app.register(todoRouter);
9
+
10
+ // setupServer (legacy):
11
+ router.use("/todos", modelRouter(Todo, options));
12
+ ```
13
+
14
+ ## Full options reference
15
+
16
+ ```typescript
17
+ modelRouter("/todos", Todo, {
18
+ permissions: {
19
+ create: [Permissions.IsAuthenticated],
20
+ list: [Permissions.IsAuthenticated],
21
+ read: [Permissions.IsOwner],
22
+ update: [Permissions.IsOwner],
23
+ delete: [], // disabled
24
+ },
25
+ queryFields: ["completed", "ownerId"],
26
+ queryFilter: OwnerQueryFilter,
27
+ sort: "-created",
28
+ defaultLimit: 100,
29
+ maxLimit: 500,
30
+ populatePaths: [{path: "ownerId", fields: ["name", "email"]}],
31
+ preCreate: (body, req) => ({...body, ownerId: req.user?._id}),
32
+ preUpdate: (body, req) => body,
33
+ postCreate: (doc, req) => {},
34
+ postUpdate: (doc, body, req, prev) => {},
35
+ postDelete: (req, doc) => {},
36
+ responseHandler: (value, method, req, options) => value,
37
+ validation: {
38
+ validateCreate: true,
39
+ validateUpdate: true,
40
+ validateQuery: true,
41
+ excludeFromCreate: ["ownerId"],
42
+ excludeFromUpdate: ["ownerId"],
43
+ },
44
+ realtime: { // deprecated; removed in Terreno 58 — use `sync` instead
45
+ methods: ["create", "update", "delete"],
46
+ roomStrategy: "owner",
47
+ },
48
+ sync: {scope: {type: "owner"}},
49
+ collectionActions: {
50
+ bulkComplete: {
51
+ method: "POST",
52
+ permissions: [Permissions.IsAuthenticated],
53
+ body: z.object({ids: z.array(z.string()).min(1)}).strict(),
54
+ response: z.object({matched: z.number(), modified: z.number()}).strict(),
55
+ summary: "Mark multiple todos complete",
56
+ handler: async ({body, user}) => { /* ... */ },
57
+ },
58
+ },
59
+ instanceActions: {
60
+ markComplete: {
61
+ method: "POST",
62
+ permissions: [Permissions.IsOwner],
63
+ summary: "Mark a single todo complete",
64
+ handler: async ({doc}) => { /* ... */ },
65
+ },
66
+ },
67
+ collectionActions: { /* named GET/POST on /todos/{name} */ },
68
+ instanceActions: { /* named GET/POST on /todos/:id/{name} */ },
69
+ // Last resort only — prefer collectionActions / instanceActions.
70
+ endpoints: (router) => {
71
+ // Custom routes registered before CRUD
72
+ },
73
+ });
74
+ ```
75
+
76
+ ## Query features
77
+
78
+ - Pagination: `?limit=20&page=2`
79
+ - Sort: `?sort=-created`
80
+ - Filters: `?completed=true` (field must be in `queryFields`)
81
+ - Complex: `?$and=[{...}]`, `?$or=[{...}]`
82
+
83
+ ## Lifecycle hooks
84
+
85
+ | Hook | When | Use for |
86
+ |------|------|---------|
87
+ | `preCreate` | Before insert | Set `ownerId`, defaults |
88
+ | `preUpdate` | Before patch | Strip immutable fields |
89
+ | `postCreate` | After insert | Side effects, notifications |
90
+ | `postUpdate` | After patch | Audit, cache invalidation |
91
+ | `postDelete` | After delete | Cleanup |
92
+
93
+ ## User casting in callbacks
94
+
95
+ ```typescript
96
+ const user = u as unknown as UserDocument;
97
+ // or in route handlers:
98
+ (req.user as unknown as UserDocument)?._id
99
+ ```
100
+
101
+ Never use `as any as UserDocument`.
102
+
103
+ ## Plugins on schemas
104
+
105
+ | Plugin | Adds |
106
+ |--------|------|
107
+ | `createdUpdatedPlugin` | `created`, `updated` timestamps |
108
+ | `isDeletedPlugin` | Soft delete (`deleted` field) |
109
+ | `findExactlyOne` | Static that throws on 0 or >1 match |
110
+ | `findOneOrNone` | Static that returns null on 0, throws on >1 |
111
+
112
+ ## OpenAPI
113
+
114
+ `setupServer` / `TerrenoApp` auto-generates `/openapi.json`. Field `description` on schema paths flows to the spec via `mongoose-to-swagger`.
115
+
116
+ After any model or router change, regenerate the frontend SDK.
@@ -0,0 +1,116 @@
1
+ # Server Setup — Terreno
2
+
3
+ ## TerrenoApp (recommended)
4
+
5
+ ```typescript
6
+ import {TerrenoApp} from "@terreno/api";
7
+ import {User} from "./models/user";
8
+ import {todoRouter} from "./api/todos";
9
+
10
+ const app = new TerrenoApp({
11
+ userModel: User,
12
+ loggingOptions: {logRequests: true, logSlowRequests: true},
13
+ })
14
+ .register(todoRouter)
15
+ .start();
16
+ ```
17
+
18
+ Methods: `.register()`, `.addMiddleware()`, `.build()`, `.start()`.
19
+
20
+ ## setupServer (legacy)
21
+
22
+ ```typescript
23
+ import {setupServer} from "@terreno/api";
24
+
25
+ setupServer({
26
+ userModel: User,
27
+ addRoutes: (router, options) => {
28
+ router.use("/todos", modelRouter(Todo, {...options, ...}));
29
+ },
30
+ });
31
+ ```
32
+
33
+ ## Required environment variables
34
+
35
+ | Variable | Purpose |
36
+ |----------|---------|
37
+ | `MONGO_URI` | MongoDB connection (replica set for change streams) |
38
+ | `TOKEN_SECRET` | JWT signing secret |
39
+ | `TOKEN_ISSUER` | JWT issuer claim |
40
+ | `REFRESH_TOKEN_SECRET` | Refresh token secret |
41
+ | `SESSION_SECRET` | Express session secret |
42
+ | `PORT` | Server port (default 4000) |
43
+
44
+ Optional: `ENABLE_SWAGGER=true`, `USE_SENTRY_LOGGING=true`, GitHub OAuth vars, Better Auth vars.
45
+
46
+ ## MongoDB for local dev
47
+
48
+ The dev server needs a real replica set (change streams). See `AGENTS.md` for the single-node replica set setup with `mongodb-memory-server` cache binary.
49
+
50
+ ```bash
51
+ MONGO_URI="mongodb://127.0.0.1:27017/terreno-example?replicaSet=rs0" \
52
+ TOKEN_SECRET=dev-token-secret TOKEN_ISSUER=terreno-dev \
53
+ REFRESH_TOKEN_SECRET=dev-refresh-secret SESSION_SECRET=dev-session-secret \
54
+ PORT=4000 bun run backend:dev
55
+ ```
56
+
57
+ Seed users: `cd example-backend && bun run seed` → `test@example.com` / `testpassword123`.
58
+
59
+ ## Health check
60
+
61
+ ```bash
62
+ curl localhost:4000/health
63
+ # {"healthy":true,...}
64
+ ```
65
+
66
+ ## OpenAPI / Swagger
67
+
68
+ - Spec: `http://localhost:4000/openapi.json`
69
+ - Swagger UI: `http://localhost:4000/swagger` (when `ENABLE_SWAGGER=true`)
70
+
71
+ ## Logging
72
+
73
+ ```typescript
74
+ import {createScopedLogger, logger} from "@terreno/api";
75
+
76
+ logger.info("Server started", {port: 4000});
77
+
78
+ const log = createScopedLogger({
79
+ prefix: "[Billing]",
80
+ labels: {invoiceId: id.toString()},
81
+ });
82
+ log.info("Processing payment");
83
+ ```
84
+
85
+ Never use `console.log` for permanent server logs.
86
+
87
+ ## Testing
88
+
89
+ ```typescript
90
+ import {describe, it, expect} from "bun:test";
91
+ import request from "supertest";
92
+ import {getBaseServer, authAsUser, setupDb} from "@terreno/api/tests";
93
+
94
+ describe("todos", () => {
95
+ it("lists todos for authenticated user", async () => {
96
+ const app = await getBaseServer();
97
+ const agent = await authAsUser(app, "notAdmin");
98
+ const res = await agent.get("/todos");
99
+ expect(res.status).toBe(200);
100
+ });
101
+ });
102
+ ```
103
+
104
+ See `backend-test-env` skill when tests mutate `process.env`.
105
+
106
+ ## Plugins
107
+
108
+ Register `TerrenoPlugin` implementations:
109
+
110
+ ```typescript
111
+ import {BetterAuthApp, type BetterAuthConfig} from "@terreno/api";
112
+ import {AdminApp} from "@terreno/admin-backend";
113
+
114
+ app.register(new BetterAuthApp({config: betterAuthConfig, userModel: User}));
115
+ admin.register(app);
116
+ ```
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: terreno-backend-tests
3
+ description: >-
4
+ Write and fix Bun tests for a Terreno backend: modelRouter routes and actions, permissions,
5
+ Mongoose models and hooks, services that call SendGrid / Twilio / Expo push / Cloud Tasks /
6
+ GCS / AI models, env-dependent code, time, and async side effects. Covers supertest +
7
+ @terreno/test helpers, auth tokens, test data, service mocks, and flake-free waiting. Use
8
+ when adding or changing backend behavior or when a backend test fails. If the app has no
9
+ test preload yet, run terreno-test-setup first.
10
+ ---
11
+
12
+ # Writing Terreno backend tests
13
+
14
+ Tests use `bun:test`, run against a real in-memory MongoDB, and call the app through HTTP with
15
+ supertest. Never mock `@terreno/api` internals or your own models — exercise the real stack.
16
+
17
+ ## 1. Pick the seam
18
+
19
+ | Behavior | Test through |
20
+ |----------|--------------|
21
+ | CRUD, actions, permissions, query filters, response shape | HTTP route (`supertest`) — default |
22
+ | Model statics, methods, hooks, validation | the model directly |
23
+ | A service with no route | its exported function |
24
+ | Third-party side effect (email, SMS, task, upload, AI call) | the route or service, asserting on the preload's mock handle |
25
+
26
+ Done when: the test file sits next to the code (`todos.ts` → `todos.test.ts`) and names the
27
+ seam in its `describe`.
28
+
29
+ ## 2. Build the app per file
30
+
31
+ ```typescript
32
+ import {beforeEach, describe, expect, it} from "bun:test";
33
+ import {generateTokens, TerrenoApp} from "@terreno/api";
34
+ import {authWithToken, expectAPIError} from "@terreno/test";
35
+ import supertest from "supertest";
36
+ import {Todo} from "../models/todo";
37
+ import {User} from "../models/user";
38
+ import {todoRouter} from "./todos";
39
+
40
+ const app = new TerrenoApp({skipListen: true, userModel: User as never})
41
+ .register(todoRouter)
42
+ .build();
43
+ ```
44
+
45
+ Register only the routers and plugins under test. Do not call the app's `start()`: it connects
46
+ to the dev database and registers everything.
47
+
48
+ ## 3. Authenticate
49
+
50
+ ```typescript
51
+ const createUser = async (email: string, admin = false) =>
52
+ User.register({admin, email, name: email} as never, "password12345");
53
+
54
+ const agentFor = async (user: unknown) =>
55
+ authWithToken(app, {token: (await generateTokens(user)).token});
56
+ ```
57
+
58
+ - `authWithToken` + `generateTokens` mints the same JWT `/auth/login` issues, without the
59
+ ~200ms password hash. Use it everywhere except tests of the login flow itself.
60
+ - `authAsUser(app, {email, password})` really logs in; keep it for login, lockout, and
61
+ password tests.
62
+ - `supertest(app)` with no token is the anonymous client.
63
+
64
+ Permission failures by layer — assert the exact status:
65
+
66
+ | Case | Status |
67
+ |------|--------|
68
+ | No or invalid token on a protected route | 401 |
69
+ | Method-level permission fails (`list`, `create`, or a disabled `[]` method) | 405 |
70
+ | Object-level permission fails (`read`, `update`, `delete` on a loaded document, e.g. `IsOwner`) | 403 |
71
+ | `list` where `queryFilter` excludes another user's documents | 200, those documents absent |
72
+ | Id that does not exist | 404 |
73
+
74
+ Write one test per role that matters: owner, other user, admin, anonymous.
75
+
76
+ ## 4. Create and clean data
77
+
78
+ - Create exactly what the test needs, in the test or a `beforeEach`, with small factory
79
+ functions (`createUser`, `createTodo({ownerId, ...overrides})`) local to the file or in
80
+ `src/tests/factories.ts`.
81
+ - The default preload does not wipe the database. Start each test clean for the collections
82
+ it reads: `beforeEach(async () => { await Todo.deleteMany({}); await User.deleteMany({}); })`.
83
+ (Apps on `registerBackendPreload({useTransactions: true})` roll back automatically; skip
84
+ the deletes there.)
85
+ - Dates are Luxon: `DateTime.fromISO("2024-06-01T10:00:00Z")`, compared with `.toISO()` or
86
+ `.toMillis()`.
87
+ - Expected values are literals from the example, never recomputed with production code.
88
+
89
+ ## 5. Assert
90
+
91
+ ```typescript
92
+ it("lets an owner list only their todos", async () => {
93
+ const owner = await createUser("owner@example.com");
94
+ const other = await createUser("other@example.com");
95
+ await Todo.create({ownerId: owner._id, title: "Mine"});
96
+ await Todo.create({ownerId: other._id, title: "Theirs"});
97
+
98
+ const res = await (await agentFor(owner)).get("/todos").expect(200);
99
+
100
+ expect(res.body.data.map((todo: {title: string}) => todo.title)).toEqual(["Mine"]);
101
+ expect(res.body.total).toBe(1);
102
+ });
103
+
104
+ it("rejects a todo without a title", async () => {
105
+ const res = await (await agentFor(await createUser("a@example.com"))).post("/todos").send({});
106
+ expectAPIError(res, {status: 400, title: /validation/i});
107
+ });
108
+ ```
109
+
110
+ - List responses are `{data, limit, more, page, total}`; single documents are `{data}`.
111
+ - `expectAPIError(res, {status, title?, detail?, code?, fields?})` matches the JSONAPI error
112
+ body; `fields` partially matches `meta.fields`.
113
+ - On routes that return user or PHI data, pin the response shape so a new model field cannot
114
+ leak: `assertStrictResponseProperties(res.body, ["data._id", "data.name", "limit", "more",
115
+ "page", "total"])`.
116
+ - After a write, re-read through the API (or `Model.findById`) to prove persistence.
117
+
118
+ ## 6. Services and side effects
119
+
120
+ Mocks are registered once in `src/tests/bunSetup.ts` and exported (see `terreno-test-setup`).
121
+ Import the handle and assert what the app sent:
122
+
123
+ ```typescript
124
+ import {cloudTasks, gcs, sendGrid} from "../tests/bunSetup";
125
+
126
+ expect(sendGrid.getSentMessages()).toEqual([
127
+ expect.objectContaining({subject: "Welcome", to: "new@example.com"}),
128
+ ]);
129
+
130
+ await cloudTasks.drainTasks({app, headers: {authorization: "Bearer task-secret"}});
131
+
132
+ gcs.putObject({bucket: "uploads", contents: "hi", name: "a.txt"});
133
+ ```
134
+
135
+ - Cloud Tasks: assert `getEnqueuedTasks()` (decoded bodies), then `drainTasks({app})` to run
136
+ the task handlers through HTTP; it also drains tasks the handlers enqueue.
137
+ - Make a provider fail with the mock's standard API, e.g.
138
+ `sendGrid.send.mockRejectedValueOnce(new Error("503"))`, and assert the app's handling.
139
+ - AI: `new AIService({model: createMockLanguageModel({responses: [{text: "Hi"}, {toolCalls:
140
+ [...]}]})})`; assert prompts via `model.doGenerate.mock.calls` / `doStream.mock.calls`.
141
+ - Never `mock.module` inside a test file: it replaces the module for every later file in the
142
+ run. Use the preload, dependency injection, or `spyOn(object, "method")` (restored
143
+ automatically after each test).
144
+
145
+ ## 7. Env, time, and async
146
+
147
+ - Env: set only the keys the test needs (`process.env.FEATURE_X = "true"`); express "unset" as
148
+ `Reflect.deleteProperty(process.env, "KEY")`. The preload re-applies the canonical env after
149
+ every test. Never snapshot and restore all of `process.env`.
150
+ - Time: `freezeTime("2024-06-01T10:30:00Z")` from `@terreno/test` freezes `Date.now()`,
151
+ `new Date()`, and Luxon for the current test and restores afterwards.
152
+ - Async side effects (post-save hooks, fire-and-forget writes, jobs): wait for the outcome, not
153
+ a duration — `await waitForDocuments(AuditEvent, {action: "create"}, 1)`,
154
+ `await waitUntil(() => sendGrid.send.mock.calls.length > 0, {label: "welcome email"})`, or
155
+ `await waitForCalls(mockFn, 2)`. Never `await new Promise((r) => setTimeout(r, n))`.
156
+
157
+ ## 8. Run
158
+
159
+ ```bash
160
+ bun test src/api/todos.test.ts # one file
161
+ bun test -t "lists only their todos" # by name
162
+ bun test --only-failures # rerun what failed last time
163
+ bun test --coverage # coverage report
164
+ ```
165
+
166
+ A test that passes alone and fails in the full run is leaking state: a module mock in a test
167
+ file, a collection not cleaned, or an env key not reset. Fix the leak; do not reorder files.
168
+
169
+ ## Checklist
170
+
171
+ - [ ] Happy path plus each rejected role/case asserts an exact status
172
+ - [ ] Data created in the test; collections it reads cleaned in `beforeEach`
173
+ - [ ] Third-party calls asserted on the preload mock handles; no `mock.module` in the file
174
+ - [ ] No sleeps; async outcomes awaited with `waitUntil` / `waitForDocuments`
175
+ - [ ] Time-dependent behavior uses `freezeTime`; env changes are targeted
176
+ - [ ] `bun test` passes for the whole backend, not just the file
@@ -0,0 +1,276 @@
1
+ ---
2
+ name: terreno-data-fetching
3
+ description: >-
4
+ Use when implementing or debugging ANY network request, API call, or data
5
+ fetching in a Terreno app. Covers syncdb collection CRUD, RTK Query generated
6
+ hooks for non-synced routes, admin's host-bound fetch client, auth, caching,
7
+ and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
8
+ application code.
9
+ Lifecycle composition: Grow for data-flow shape, Pick for implementation, Roast
10
+ for independent network/cache behavior proof.
11
+ ---
12
+ # Terreno Data Fetching
13
+
14
+ **You MUST use this skill for ANY networking work in Terreno apps.** Use syncdb
15
+ for synced collection CRUD and generated RTK Query hooks for non-synced routes.
16
+ Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
17
+
18
+ **Related skills:** `generate-sdk` (regenerate hooks after backend changes), `terreno-backend-api` (create backend routes), `building-terreno-apps` (screen integration).
19
+
20
+ ## Documentation
21
+
22
+ 1. Read `docs/reference/syncdb.md`, `docs/how-to/migrate-rtk-to-syncdb.md`, `docs/how-to/use-syncdb-correctly.md`, and auth/realtime explanation pages before changing fetch, cache, or session behavior.
23
+ 2. Implement against that design.
24
+ 3. Update those pages in the same slice with `update-docs`.
25
+ 4. Ship without matching docs is a failed slice.
26
+
27
+ ## References
28
+
29
+ ```
30
+ references/
31
+ auth-and-tokens.md generateAuthSlice, login/logout, token refresh
32
+ realtime.md Sockets, feature flags, server status
33
+ sdk-customization.md injectEndpoints, cache tags, hand-maintained sdk.ts
34
+ ```
35
+
36
+ ## When to Use
37
+
38
+ - Implementing API requests in screens or hooks
39
+ - Setting up authentication (login, logout, token refresh)
40
+ - Debugging network failures or stale data
41
+ - Connecting realtime updates (sockets, feature flags)
42
+ - Configuring API URLs and environment variables
43
+ - After any backend route or model change (regenerate SDK)
44
+
45
+ ## Core Rules
46
+
47
+ 1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useMutate`) for synced collection CRUD.
48
+ 2. **Use generated hooks** from `store/openApiSdk.ts` (or `store/sdk.ts`) for non-synced routes.
49
+ 3. **Never edit `openApiSdk.ts` manually** — regenerate with `generate-sdk`.
50
+ 4. **Never use `axios` or raw `fetch`** in app code. Framework-owned admin RPC
51
+ is the exception: `@terreno/admin-frontend` binds `adminRequest` from host
52
+ `credentials` / `getAuthHeaders`.
53
+ 5. **Do not add new admin `injectEndpoints`.** String-`_id` models with
54
+ `adminBroadcast` use windowed syncdb; ObjectId compatibility CRUD remains RTK
55
+ until Terreno 58 removes `useAdminApi` and the required `api` prop.
56
+
57
+ ## Quick Start
58
+
59
+ ### Synced collection read
60
+
61
+ ```tsx
62
+ import {useQuery} from "@terreno/syncdb/react";
63
+
64
+ const TodosScreen: React.FC = () => {
65
+ const todos = useQuery<Todo>("todos", {
66
+ filter: (todo) => !todo.completed,
67
+ });
68
+ };
69
+ ```
70
+
71
+ ### Synced collection write
72
+
73
+ ```tsx
74
+ import {useMutate} from "@terreno/syncdb/react";
75
+
76
+ const {create, update} = useMutate("todos");
77
+
78
+ const handleCreate = useCallback((): void => {
79
+ create({data: {title: "New todo", completed: false}});
80
+ }, [create]);
81
+
82
+ const handleToggle = useCallback((id: string, completed: boolean): void => {
83
+ update({id, data: {completed: !completed}});
84
+ }, [update]);
85
+ ```
86
+
87
+ ### Auth
88
+
89
+ ```tsx
90
+ import {generateAuthSlice} from "@terreno/rtk";
91
+ import {terrenoApi} from "@/store/sdk";
92
+
93
+ const {authReducer, logout, middleware} = generateAuthSlice(terrenoApi);
94
+
95
+ // In a component:
96
+ import {useEmailLoginMutation} from "@/store/sdk";
97
+ import {useSelectCurrentUserId} from "@terreno/rtk";
98
+
99
+ const [login] = useEmailLoginMutation();
100
+ const userId = useSelectCurrentUserId();
101
+ ```
102
+
103
+ See `./references/auth-and-tokens.md` for the full auth flow.
104
+
105
+ ## Redux Store Setup
106
+
107
+ ```tsx
108
+ import {configureStore} from "@reduxjs/toolkit";
109
+ import {generateAuthSlice} from "@terreno/rtk";
110
+ import {terrenoApi} from "./sdk";
111
+
112
+ const authSlice = generateAuthSlice(terrenoApi);
113
+
114
+ const store = configureStore({
115
+ reducer: {
116
+ auth: authSlice.authReducer,
117
+ [terrenoApi.reducerPath]: terrenoApi.reducer,
118
+ },
119
+ middleware: (getDefault) =>
120
+ getDefault().concat(terrenoApi.middleware, ...authSlice.middleware),
121
+ });
122
+
123
+ export const {logout} = authSlice;
124
+ ```
125
+
126
+ See `example-frontend/store/index.ts` for persist and Sentry integration.
127
+
128
+ ## Environment Variables
129
+
130
+ | Variable | Purpose |
131
+ |----------|---------|
132
+ | `EXPO_PUBLIC_API_URL` | Backend URL for web dev (e.g. `http://localhost:4000`) |
133
+ | `EXPO_PUBLIC_DEV_API_PORT` | Dev API port when using Expo host URI (default `4000`) |
134
+ | `expo.extra.BASE_URL` | Production/staging API URL in `app.json` |
135
+
136
+ Base URL resolution priority (from `@terreno/rtk`):
137
+
138
+ 1. `expo.extra.BASE_URL`
139
+ 2. `EXPO_PUBLIC_API_URL`
140
+ 3. Expo dev server host + dev API port
141
+ 4. `http://localhost:4000`
142
+
143
+ Never put server secrets in `EXPO_PUBLIC_*` variables.
144
+
145
+ ## Error Handling
146
+
147
+ RTK Query exposes errors on the hook result:
148
+
149
+ ```tsx
150
+ const {error, isError} = useGetMeQuery();
151
+
152
+ if (isError) {
153
+ // error is FetchBaseQueryError | SerializedError
154
+ console.error("Query failed", error);
155
+ }
156
+ ```
157
+
158
+ For mutations, use `.unwrap()`:
159
+
160
+ ```tsx
161
+ try {
162
+ await updateMe({name}).unwrap();
163
+ } catch (err) {
164
+ // Handle APIError-shaped responses from @terreno/api
165
+ console.error("Mutation failed", err);
166
+ }
167
+ ```
168
+
169
+ Use `isNetworkFetchError` from `@terreno/rtk` to detect connectivity issues in error UI.
170
+
171
+ ## SDK Regeneration
172
+
173
+ After a non-synced backend API change, run the `generate-sdk` skill. Synced
174
+ collection CRUD does not belong in the OpenAPI SDK.
175
+
176
+ ```bash
177
+ # Backend running on :4000, then:
178
+ cd example-frontend && bun run sdk
179
+ ```
180
+
181
+ Triggers: custom routes, non-synced `modelRouter` routes, and OpenAPI builder
182
+ edits. Regenerate syncdb collection types/hooks with its codegen when a synced
183
+ schema changes.
184
+
185
+ ## Decision Tree
186
+
187
+ ```
188
+ Need data in a Terreno app?
189
+ |-- Synced collection CRUD?
190
+ | \-- @terreno/syncdb hooks
191
+ |
192
+ |-- Non-synced API route?
193
+ | \-- useGet*/usePost* from generated SDK
194
+ |
195
+ |-- Built-in admin?
196
+ | |-- String id + adminBroadcast -> windowed syncdb
197
+ | |-- Framework RPC -> host-bound adminRequest
198
+ | \-- ObjectId compatibility CRUD -> useAdminApi until Terreno 58
199
+ |
200
+ |-- Login/logout?
201
+ | \-- references/auth-and-tokens.md
202
+ |
203
+ |-- Realtime updates?
204
+ | \-- references/realtime.md (useSocketConnection)
205
+ |
206
+ |-- Custom endpoint not in SDK?
207
+ | |-- Backend exists? -> generate-sdk first
208
+ | \-- App-only extension? -> references/sdk-customization.md
209
+ |
210
+ |-- Backend doesn't exist?
211
+ | \-- terreno-backend-api, then generate-sdk
212
+ |
213
+ \-- Using axios/direct fetch/React Query?
214
+ \-- STOP — choose syncdb or a generated hook
215
+ ```
216
+
217
+ ## Common Mistakes
218
+
219
+ **Wrong: axios**
220
+
221
+ ```tsx
222
+ const result = await axios.get("/todos");
223
+ ```
224
+
225
+ **Right: syncdb for a synced collection**
226
+
227
+ ```tsx
228
+ const todos = useQuery<Todo>("todos");
229
+ ```
230
+
231
+ **Wrong: manual token in fetch**
232
+
233
+ ```tsx
234
+ fetch(url, {headers: {Authorization: `Bearer ${token}`}});
235
+ ```
236
+
237
+ **Right: a generated hook handles auth for a non-synced route**
238
+
239
+ ```tsx
240
+ const {data: profile} = useGetMeQuery();
241
+ ```
242
+
243
+ **Wrong: editing openApiSdk.ts**
244
+
245
+ ```tsx
246
+ // Adding a new endpoint by hand
247
+ ```
248
+
249
+ **Right: regenerate**
250
+
251
+ ```bash
252
+ cd example-frontend && bun run sdk
253
+ ```
254
+
255
+ **Wrong: refusing a synced write with 403 from a router hook**
256
+
257
+ ```typescript
258
+ throw new APIError({status: 403, title: "Not a member"}); // in preCreate — halts the client outbox
259
+ ```
260
+
261
+ **Right: 422, so syncdb nacks it as `validation`**
262
+
263
+ ```typescript
264
+ throw new APIError({status: 422, title: "Not a member"});
265
+ ```
266
+
267
+ **Wrong: rendering the empty state from `useQuery` before the stream bootstraps.** An empty array before bootstrap looks the same as an empty collection. Gate on the stream's `bootstrapped` cell. See `docs/how-to/use-syncdb-correctly.md` for this and the other pitfalls (cold-start cost per stream, dropped ack warnings, phantom creates, unresolved conflicts).
268
+
269
+ ## Example Invocations
270
+
271
+ - "How do I fetch todos?" → `useQuery("todos")` from `@terreno/syncdb/react`
272
+ - "How do I handle login?" → `useEmailLoginMutation` + `generateAuthSlice`
273
+ - "Should I use React Query?" → No — syncdb for collections, generated RTK hooks for non-synced routes
274
+ - "API calls return 401" → Check token refresh; see auth-and-tokens.md
275
+ - "Hooks are missing after backend change" → Run `generate-sdk` skill
276
+ - "How do I add a custom endpoint?" → Backend route + OpenAPI, then regenerate the SDK
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Terreno Data Fetching"
3
+ short_description: "RTK Query generated hooks, auth tokens, and realtime sockets for Terreno apps — never axios or raw fetch"
4
+ default_prompt: "Use $terreno-data-fetching to implement API calls with generated RTK Query hooks, auth, and caching in a Terreno app."