@terreno/agent-plugin 0.0.0-stage → 58.0.0-beta.2

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 +2213 -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 +86 -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 +74 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +294 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +108 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +104 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +174 -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 +165 -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 +112 -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 +288 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +89 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +50 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +55 -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 +87 -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 +93 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: backend-test-env
3
+ description: >-
4
+ Invoke when writing tests that mutate `process.env` in `@terreno/api`,
5
+ `@terreno/ai`, or `example-backend` — feature flags, auth secrets, integration
6
+ toggles. Covers the preload contract, `setupEnvironment()`, and safe mutation
7
+ patterns. Lifecycle composition: Pick when writing these tests; Roast when
8
+ independently reproducing env-sensitive behavior.
9
+ ---
10
+ # Backend Test — `process.env` Contract
11
+
12
+ Bun's test runner preloads a per-package setup file (configured in each package's `bunfig.toml` under `[test].preload`):
13
+
14
+ | Package | Preload file |
15
+ |---------|--------------|
16
+ | `@terreno/api` | `api/src/tests/bunSetup.ts` |
17
+ | `@terreno/admin-backend` | `admin-backend/src/tests/testEnv.ts` then `api/src/tests/bunSetup.ts` (`testEnv` sets `TERRENO_TEST_USE_MEMORY_MONGO` for **mongodb-memory-server**) |
18
+ | `@terreno/ai` | `ai/src/tests/bunSetup.ts` |
19
+ | `@terreno/comms` | `comms/src/tests/bunSetup.ts` |
20
+ | `example-backend` | `example-backend/src/tests/setup.ts` |
21
+
22
+ The `@terreno/api` preload uses a global `beforeEach` to (1) reset the canonical auth secrets, (2) call `setupEnvironment()` (exported from `expressServer.ts`) for the wider baseline, and (3) re-silence the winston loggers. The next test always starts from the canonical baseline — you normally **do not** need to re-call `setupEnvironment()` in a file-level `beforeEach`.
23
+
24
+ **Mongo in tests:** `api/src/tests/bunSetup.ts` connects to `mongodb://127.0.0.1/terreno` by default. If `TERRENO_TEST_USE_MEMORY_MONGO=true`, it starts **MongoMemoryServer** and connects to that URI instead (then stops it in `afterAll`). `TERRENO_TEST_MONGODB_URI` overrides both and points at any MongoDB you choose.
25
+
26
+ ## Default — Rely on the Preload
27
+
28
+ - Do not assume a toggle left by another file. Treat the preload's `beforeEach` as the source of truth for the canonical baseline.
29
+ - Set only the keys this test needs in its own `beforeEach` or test body.
30
+ - Don't reach in and call `setupEnvironment()` again unless you're explicitly testing env-driven behavior that the preload didn't cover.
31
+
32
+ ## When a Test Mutates `process.env`
33
+
34
+ 1. **Add the key to the preload's reset list** if it should always start from a fixed test value. Otherwise it leaks into the next test in the same worker.
35
+ 2. **Set only what this case needs** in the test body — avoid redundant manual restore if the preload already resets that key in `beforeEach`.
36
+ 3. **For keys that should be absent (unset)**, use **`Reflect.deleteProperty(process.env, "KEY")`**. Avoid `process.env.KEY = ""` to mean "unset" — on some platforms empty string and unset behave differently and can make tests flaky.
37
+ 4. **Avoid whole-env snapshots** (`const OLD_ENV = process.env; ...; process.env = OLD_ENV;`). This pattern exists in some older test files (`auth.test.ts`) — don't propagate it. It captures pre-`beforeEach` state and breaks ordering guarantees if the preload's `beforeEach` runs in between. Use targeted mutations + the preload's reset instead.
38
+
39
+ ## Adding a New Required Env Var
40
+
41
+ If you introduce a new `process.env.FOO` that backend code reads at startup:
42
+
43
+ 1. Add `FOO` to `setupEnvironment()` in `api/src/expressServer.ts` so every package picks it up via the preload's `setupEnvironment()` call.
44
+ 2. If `example-backend` has its own startup needs for it, also add to `example-backend/src/tests/setup.ts`'s `beforeAll`.
45
+ 3. If a test needs to mutate it, follow the rules above — don't snapshot the whole env.
46
+
47
+ ## Auth-Related Keys
48
+
49
+ `api/src/tests/bunSetup.ts` resets four auth keys in `beforeEach`: `TOKEN_SECRET`, `TOKEN_ISSUER`, `SESSION_SECRET`, `REFRESH_TOKEN_SECRET`. If your test changes any of these, you do not need to restore — the next `beforeEach` already does. If your test relies on them being **absent** for a code path, use `Reflect.deleteProperty` within the test, knowing the next `beforeEach` will restore.
50
+
51
+ `auth.test.ts` is the established home for env-driven auth assertions; mirror its newer patterns (per-test `beforeEach` setting only what changes) rather than its older whole-env-snapshot blocks.
52
+
53
+ ## Log Capture & Sentry
54
+
55
+ The preload silences winston and `console.*` and captures into an in-memory buffer (cleared in `beforeEach`/`afterEach`). It also mocks `@sentry/bun`. Don't re-mock Sentry inside individual tests — the preload's mock is shared and reset across tests.
56
+
57
+ ## Checklist
58
+
59
+ - [ ] New env var added to `setupEnvironment()` and (if needed) `example-backend/src/tests/setup.ts`
60
+ - [ ] Test mutations are targeted — no whole-env snapshots
61
+ - [ ] "Unset" expressed as `Reflect.deleteProperty(process.env, "KEY")`, not `= ""`
62
+ - [ ] No manual restore that duplicates the preload's `beforeEach` reset
63
+ - [ ] No re-mock of `@sentry/bun` — the preload's mock is already in place
64
+ - [ ] If you added an env-driven branch in code, a test covers both the set and the unset case
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: backfill-regression
3
+ description: >-
4
+ Backfill a repository's manual regression suites. Survey the app for behavior that only a
5
+ developer or QA can check by hand (push notifications and other hardware or OS behavior,
6
+ real email or SMS delivery, full cross-device flows, how the app feels) plus a short check
7
+ that the app works as intended, propose candidate cases one area at a time, and have the
8
+ human approve, edit, or deny each set before anything is written. Loops until no
9
+ candidates remain or the human stops. Use when asked to backfill, seed, or audit regression
10
+ tests or a manual QA checklist; not for planning a feature (Grow writes those cases) or for
11
+ writing automated tests.
12
+ ---
13
+ # Backfill regression
14
+
15
+ Build the minimal manual pass a developer or QA walks before a release. Every case is
16
+ approved by the human before it is written. The suites stay small: fewer, sharper cases
17
+ beat coverage.
18
+
19
+ Read [`testing-plan`](../../references/testing-plan.md) first. Its "Keep regression
20
+ minimal" section decides what may be a case, its suite file format decides how a case is
21
+ written, and its regression-step bar decides whether a case is good enough.
22
+
23
+ ## Inputs
24
+
25
+ - The resolved `E2E guide`, `Regression suites`, and `Smoke command` per the testing plan
26
+ - Every existing suite file, its `Area:` prefix, and its highest ID
27
+ - The ledger at `.terreno/regression-backfill.json`, when present (see Ledger)
28
+ - Optional scope from the human: an area, a package, or a list of features
29
+
30
+ ## Procedure
31
+
32
+ When `Regression suites` resolves to `none` (the testing-plan opt-out), stop: report that
33
+ the repository opted out of manual regression suites, and write nothing.
34
+
35
+ 1. **Survey.** Use explore subagents in parallel. Collect evidence of behavior automation
36
+ cannot reach, each with a file and line:
37
+ - hardware and OS: push registration and delivery, notification permission, camera,
38
+ photo library, location, biometrics, secure storage across reinstall, deep and
39
+ universal links from outside the app, background and resume
40
+ - delivery outside the app: email, SMS, and push that must reach a real inbox or device
41
+ - full flows across apps, devices, or channels: a change on one device showing on
42
+ another, an admin action reaching a user's phone, offline then online
43
+ - feel: gestures, drawing, keyboard avoidance, animation, layout on small screens
44
+ - the app works: sign in, the main screen loads, the core action succeeds, on each
45
+ shipped platform. One short case per platform, not per screen.
46
+ 2. **Filter.** Drop a candidate when an existing suite case already covers it, when an
47
+ e2e spec, smoke, or unit test already proves it, when a guide layer could drive and
48
+ assert it (note it as an automation gap in the final report instead), or when the
49
+ ledger records it as denied. Merge near-duplicates into one case.
50
+ 3. **Group into sets.** One set per area, at most five cases, highest risk first. A
51
+ set names its destination suite file, existing or new, with its `Area:` prefix.
52
+ Write each case in the suite table format with a provisional `<AREA>-new-<n>` ID,
53
+ `Automated by: manual`, and `Added: regression-backfill`. Every case meets the
54
+ regression-step bar, and its evidence line explains why it cannot be automated.
55
+ 4. **Ask, one set at a time.** Show the set as a table with each case's evidence, and
56
+ ask the human, using the structured question tool when available:
57
+ - **Approve all**: write every case in the set.
58
+ - **Approve some**: the human names the case IDs to keep; deny the rest.
59
+ - **Edit**: the human's changes are applied, and the set is shown again.
60
+ - **Deny all**: write nothing for this set.
61
+ - **Stop**: end the loop now.
62
+ Never write a case the human has not approved.
63
+ 5. **Write approved cases.** Append them to the suite file with real IDs: one past the
64
+ highest ID the file has ever held, never reused. A new suite file follows the suite
65
+ file format, with `Last full run: never` and `Owner:` from the human or `unassigned`.
66
+ Add a new area to the suite index when the repository keeps one. Run the repository's
67
+ suite check when it has one, and fix any failure before the next set.
68
+ 6. **Record.** Append each case to the ledger as approved or denied, then go to the next
69
+ set. When every set is answered, survey once more for areas the answers opened up;
70
+ stop when a pass yields no new candidate.
71
+
72
+ ## Ledger
73
+
74
+ `.terreno/regression-backfill.json` stays out of version control: add `.terreno/` to
75
+ `.gitignore` when it is missing. It keeps a denied case from being proposed again:
76
+
77
+ ```json
78
+ {"cases": [{"area": "TODO", "case": "A todo shows on a second device", "decision": "denied", "date": "2026-01-15"}]}
79
+ ```
80
+
81
+ Match a candidate against the ledger by area and meaning, not exact wording.
82
+
83
+ ## Report
84
+
85
+ When the loop ends, report once:
86
+
87
+ - cases written, per suite file, with their real IDs
88
+ - sets and cases denied
89
+ - automation gaps: candidates a guide layer could cover, each with the layer and the
90
+ spec path the guide prescribes, offered as follow-up work, not built
91
+ - the suite check result
92
+
93
+ Do not commit or push unless the human asks. A commit message says which suites gained
94
+ cases.
95
+
96
+ ## Never
97
+
98
+ - write a case the human did not approve, or re-propose one the ledger records as denied
99
+ - add a case for something an automated test can prove
100
+ - walk a case or change `Last full run`; only a developer or QA does that
101
+ - renumber or delete an existing case
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: backfill-user-guide
3
+ description: >-
4
+ Backfill a repository's end-user guide. Survey the shipped app for user-facing features
5
+ (screens, navigation, forms, settings, admin tools) that have no guide page, draft pages in
6
+ the user-guide format one area at a time, and have the human approve, edit, or deny each set
7
+ before anything is written. Loops until every shipped feature has a page or a recorded
8
+ denial, or the human stops. Use when asked to backfill, seed, or audit the user guide, help
9
+ pages, or "how do I" docs for chat agents; not for planning a feature (Grow plans those
10
+ pages) or for developer docs.
11
+ ---
12
+ # Backfill user guide
13
+
14
+ Write the pages a user, or a chat agent answering for one, needs to use the features the
15
+ app already ships. Every page is approved by the human before it is written.
16
+
17
+ Read [`user guide`](../../references/user-guide.md) first. Its page format decides how a
18
+ page is written, its guide-entry bar decides whether a page is good enough, and its opt-out
19
+ rule decides whether to run at all: when the repository's resolved `User guide` is `none`,
20
+ report that the guide is off and stop.
21
+
22
+ ## Inputs
23
+
24
+ - The resolved `User guide` path per the user-guide contract, its `README.md` index, and
25
+ every existing page with its `area` and `questions`
26
+ - The ledger at `.terreno/user-guide-backfill.json`, when present (see Ledger)
27
+ - The repository's guide check command, when it has one
28
+ - Optional scope from the human: an area, an app, or a list of features
29
+
30
+ ## Procedure
31
+
32
+ 1. **Survey.** Use explore subagents in parallel, one per app or top-level area. Collect
33
+ each shipped user-facing feature with evidence (a file and line):
34
+ - every routed screen and tab, and how a user reaches it
35
+ - every action a user takes there: create, edit, delete, search, filter, share, export
36
+ - account flows: sign up, sign in, sign out, password reset, profile, settings,
37
+ consent, notifications, deleting an account
38
+ - staff and admin tools, recorded with `audience: admin` or `staff`
39
+ - feature flags and platform checks that hide a feature from some users or platforms
40
+ Read the on-screen labels from the source (button text, titles, headings), never from
41
+ memory, and note which platforms render each screen. When the repository has an
42
+ inventory command for the guide, run it to list screens no page covers.
43
+ 2. **Filter.** Drop a feature when an existing page already covers it, when it is not
44
+ reachable by any user (dead code, a flag that is off everywhere, a developer-only
45
+ screen), or when the ledger records it as denied. Merge features a user thinks of as
46
+ one task into one page.
47
+ 3. **Group into sets.** One set per area, at most five pages, most-used features first.
48
+ Draft each page in full in the page format, with `added: user-guide-backfill`. Every
49
+ page meets the guide-entry bar: user words only, every step names a real on-screen
50
+ label, and nothing a chat agent must not repeat (personal data, secrets, internal
51
+ URLs, staff procedures on a `user` page).
52
+ 4. **Ask, one set at a time.** Show each page's title, audience, platforms, questions, and
53
+ steps, with the evidence for its labels, and ask the human, using the structured
54
+ question tool when available:
55
+ - **Approve all**: write every page in the set.
56
+ - **Approve some**: the human names the pages to keep; deny the rest.
57
+ - **Edit**: the human's changes are applied, and the set is shown again.
58
+ - **Deny all**: write nothing for this set.
59
+ - **Stop**: end the loop now.
60
+ Never write a page the human has not approved.
61
+ 5. **Write approved pages.** Write each page at `<User guide>/<area>/<feature>.md` and add
62
+ it to the index table. A missing guide directory gets its `README.md` index first. Run
63
+ the repository's guide check when it has one, and fix any failure before the next set.
64
+ 6. **Record.** Append each page to the ledger as approved or denied, then go to the next
65
+ set. When every set is answered, survey once more for features the answers opened up;
66
+ stop when a pass yields no new candidate.
67
+
68
+ ## Ledger
69
+
70
+ `.terreno/user-guide-backfill.json` stays out of version control: add `.terreno/` to
71
+ `.gitignore` when it is missing. It keeps a denied page from being proposed again:
72
+
73
+ ```json
74
+ {"pages": [{"area": "todos", "title": "Export todos as CSV", "decision": "denied", "date": "2026-01-15"}]}
75
+ ```
76
+
77
+ Match a candidate against the ledger by area and meaning, not exact wording.
78
+
79
+ ## Report
80
+
81
+ When the loop ends, report once:
82
+
83
+ - pages written, per area, with their paths
84
+ - sets and pages denied
85
+ - features left without a page, and why
86
+ - the guide check result
87
+
88
+ Do not commit or push unless the human asks. A commit message says which areas gained
89
+ pages.
90
+
91
+ ## Never
92
+
93
+ - write a page the human did not approve, or re-propose one the ledger records as denied
94
+ - describe a feature that does not ship, or a label you did not read in the source
95
+ - put personal or health information, real accounts, secrets, or internal URLs in a page
96
+ - run when the repository opts out of the user guide
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: bootstrap-mcp-server
3
+ description: Bootstrap a backend-only Terreno MCP server that ships as a single Bun executable with the admin console embedded. Scaffolds the backend with create-terreno-app, drops the frontend, registers MCP tools, service tokens, optional MCP OAuth using the admin console's consent screen, a request log, tool on/off switches, and the MCP admin screens, then compiles everything (admin SPA included) into one binary. Trigger with "bootstrap an MCP server", "new Terreno MCP server", "MCP-only backend", or "ship the MCP server as a binary". Not for a full Expo app (use the terreno_bootstrap prompt / create-terreno-app) or for adding MCP tools to an existing app (see https://terreno-docs.netlify.app/how-to/expose-mcp-tools).
4
+ targets: ['*']
5
+ ---
6
+
7
+ # Bootstrap an MCP server
8
+
9
+ The MCP-only sibling of the `terreno_bootstrap` prompt. Output: one directory with a Terreno
10
+ backend whose product surface is `POST /mcp`, an admin console at `/console`, and a
11
+ `bun run build:binary` script that emits **one executable** with the admin SPA
12
+ embedded. MongoDB stays external (a replica set, as for every Terreno backend).
13
+
14
+ Requires the `@terreno/*` release that ships `mcpAdminModels`, `mcpRequestLog`,
15
+ `mcpToolSettings`, and the admin SPA consent route (check `CHANGELOG.md` for "MCP admin screens";
16
+ `create-terreno-app` scaffolds that version or newer).
17
+
18
+ ## When to use
19
+
20
+ - The user wants a remote MCP server (Claude.ai / ChatGPT connectors, Claude Code, Cursor,
21
+ Perplexity) backed by Terreno auth, permissions, and models.
22
+ - The user wants that server deployable as a single file (`scp` + `systemd`, a tiny container,
23
+ or a homelab box behind a Cloudflare Tunnel).
24
+
25
+ ## Inputs
26
+
27
+ Ask only for what is missing:
28
+
29
+ 1. `appName` (kebab-case) and `appDisplayName`.
30
+ 2. Public origin the server will be reached at (for example `https://mcp.example.com`). Needed
31
+ for `PUBLIC_API_URL`, the OAuth `issuer`, and the Better Auth URL. Use
32
+ `http://localhost:4000` until the user has one.
33
+ 3. Which clients must connect: static-key clients (Perplexity, Cursor, Claude Code) need
34
+ **service tokens**; Claude.ai / ChatGPT connectors need **MCP OAuth**. Default: both.
35
+
36
+ ## Steps
37
+
38
+ Complete each step before the next. Each ends with a check.
39
+
40
+ ### 1. Scaffold the backend only
41
+
42
+ ```bash
43
+ bunx create-terreno-app <appName> --display-name "<appDisplayName>"
44
+ cd <appName>
45
+ rm -rf frontend # MCP-only: the admin console is the only UI
46
+ ```
47
+
48
+ Remove `frontend` from the root workspace list and root scripts. Check: `bun install` succeeds
49
+ and `cd backend && bun run compile` (or `bunx tsc --noEmit`) passes.
50
+
51
+ ### 2. Add the admin packages
52
+
53
+ ```bash
54
+ cd backend
55
+ bun add @terreno/admin-backend @terreno/admin-spa
56
+ ```
57
+
58
+ Register, in this order, after `BetterAuthApp`:
59
+
60
+ ```typescript
61
+ import {AdminApp, mcpAdminModels, mcpAdminScripts} from "@terreno/admin-backend";
62
+ import {AdminSpaServeApp} from "@terreno/admin-spa";
63
+
64
+ terreno
65
+ .register(
66
+ new AdminApp({
67
+ models: [...appAdminModels, ...mcpAdminModels({requestLog: true, toolSettings: true})],
68
+ scripts: mcpAdminScripts(),
69
+ })
70
+ )
71
+ .register(
72
+ new AdminSpaServeApp({
73
+ appConfig: {brandName: APP_NAME, providers: ["email"]},
74
+ basePath: "/console",
75
+ // Set by src/embeddedAssets.ts in the compiled binary; undefined in dev.
76
+ distDir: process.env.ADMIN_SPA_DIST_DIR,
77
+ })
78
+ );
79
+ ```
80
+
81
+ `mcpAdminModels({requestLog: true, toolSettings: true})` adds the **MCP** sidebar group (service
82
+ tokens, OAuth clients, OAuth connections, request log, tools; keep the two flags matched to the
83
+ `TerrenoApp` options) and `mcpAdminScripts()` adds `mcp-sign-out-everywhere`. If the
84
+ app uses RBAC (`accessControl`), spread `mcpAdminStatements` into its statements. See
85
+ https://terreno-docs.netlify.app/how-to/manage-mcp-in-admin. Check: `curl -I localhost:4000/console` returns 200 (the
86
+ published package ships the prebuilt SPA).
87
+
88
+ ### 3. Turn on the MCP surface
89
+
90
+ In the `TerrenoApp` options:
91
+
92
+ ```typescript
93
+ mcpServiceTokens: {enabled: true, publicMcpUrl: process.env.PUBLIC_API_URL},
94
+ mcpOAuth: process.env.MCP_OAUTH_ENABLED === "true"
95
+ ? {
96
+ allowedRedirectHosts: ["claude.ai", "chatgpt.com"],
97
+ consentUrl: `${process.env.PUBLIC_API_URL}/console/oauth/consent`,
98
+ dynamicClientRegistration: true,
99
+ enabled: true,
100
+ issuer: process.env.PUBLIC_API_URL!,
101
+ }
102
+ : undefined,
103
+ mcpRequestLog: {enabled: true, retentionDays: 30}, // metadata only, never tool arguments
104
+ mcpToolSettings: true, // admin on/off switch per tool
105
+ rateLimit: {store: "memory", trustProxy: "loopback"}, // registration and token endpoints are unauthenticated
106
+ ```
107
+
108
+ Expose tools with `mcp: {methods: ["list", "read"]}` on `modelRouter` and custom tools with
109
+ `registerMCPTool` (see https://terreno-docs.netlify.app/how-to/expose-mcp-tools). Ship at least one tool, or `/mcp` is
110
+ not mounted.
111
+
112
+ **Consent screen (OAuth only).** `consentUrl` points at the admin console's built-in consent
113
+ screen (`@terreno/admin-spa`, route `/oauth/consent`). Any signed-in user can approve there, not
114
+ only admins; a signed-out user signs in and comes back. Keep the `/console` part equal to
115
+ `AdminSpaServeApp`'s `basePath`. Check: `curl -s -X POST localhost:4000/mcp` returns 401 with a
116
+ `WWW-Authenticate: Bearer resource_metadata=...` header when OAuth is on.
117
+
118
+ ### 4. Check the MCP admin screens
119
+
120
+ Seed an admin, sign in at `/console`, and open the **MCP** group. Check: **MCP tools** lists every
121
+ tool with `enabled: true`; switching one off removes it from `tools/list`.
122
+
123
+ ### 5. Embed the admin console in the binary
124
+
125
+ `bun build --compile` resolves `__dirname` to `$bunfs`, so `AdminSpaServeApp` cannot find the
126
+ SPA bundle on disk. The published `@terreno/admin-spa` already ships its prebuilt export in
127
+ `node_modules/@terreno/admin-spa/dist`; embed that and extract it at boot. No Expo or Node build
128
+ step is needed.
129
+
130
+ `scripts/generateEmbeddedAssets.ts` (writes a manifest of `with {type: "file"}` imports):
131
+
132
+ ```typescript
133
+ import {readdirSync, statSync, writeFileSync} from "node:fs";
134
+ import {join, relative, resolve} from "node:path";
135
+
136
+ const root = resolve(process.argv[2] ?? "node_modules/@terreno/admin-spa/dist");
137
+ const files: string[] = [];
138
+ const walk = (dir: string): void => {
139
+ for (const name of readdirSync(dir)) {
140
+ const path = join(dir, name);
141
+ if (statSync(path).isDirectory()) {
142
+ walk(path);
143
+ } else {
144
+ files.push(path);
145
+ }
146
+ }
147
+ };
148
+ walk(root);
149
+ const lines = files.map((file, i) => `import f${i} from ${JSON.stringify(file)} with {type: "file"};`);
150
+ lines.push(
151
+ `export const ADMIN_SPA_FILES: {path: string; file: string}[] = [${files
152
+ .map((file, i) => `{path: ${JSON.stringify(relative(root, file))}, file: f${i}}`)
153
+ .join(",\n")}];`
154
+ );
155
+ writeFileSync("src/embeddedAdminSpa.generated.ts", `${lines.join("\n")}\n`);
156
+ ```
157
+
158
+ Commit `src/embeddedAdminSpa.generated.ts` as this stub so a fresh clone typechecks and
159
+ `bun run dev` works. `build:binary` (step 6) restores it after compiling:
160
+
161
+ ```typescript
162
+ export const ADMIN_SPA_FILES: {path: string; file: string}[] = [];
163
+ ```
164
+
165
+ `src/embeddedAdminSpa.ts`, imported at the top of `src/index.ts` before the server module:
166
+
167
+ ```typescript
168
+ import {mkdirSync, writeFileSync} from "node:fs";
169
+ import {tmpdir} from "node:os";
170
+ import {dirname, join} from "node:path";
171
+ import {ADMIN_SPA_FILES} from "./embeddedAdminSpa.generated";
172
+
173
+ // Extract the embedded admin bundle once per process so AdminSpaServeApp can serve it from disk.
174
+ if (ADMIN_SPA_FILES.length > 0 && !process.env.ADMIN_SPA_DIST_DIR) {
175
+ const out = join(tmpdir(), `<appName>-admin-spa-${process.pid}`);
176
+ for (const {path, file} of ADMIN_SPA_FILES) {
177
+ const dest = join(out, path);
178
+ mkdirSync(dirname(dest), {recursive: true});
179
+ writeFileSync(dest, new Uint8Array(await Bun.file(file).arrayBuffer()));
180
+ }
181
+ process.env.ADMIN_SPA_DIST_DIR = out;
182
+ }
183
+ ```
184
+
185
+ With the stub (dev), nothing is extracted and `AdminSpaServeApp` uses its package default. If the
186
+ app later adds migrations, embed `migrations/` the same way and set `MIGRATIONS_DIR`.
187
+
188
+ ### 6. Build the binary
189
+
190
+ Add to `backend/package.json`:
191
+
192
+ ```json
193
+ "build:binary": "bun scripts/generateEmbeddedAssets.ts && bun build --compile --minify src/index.ts --outfile dist/<appName> && git checkout src/embeddedAdminSpa.generated.ts"
194
+ ```
195
+
196
+ Cross-compile with `--target=bun-linux-x64` (or `bun-linux-arm64`, `bun-darwin-arm64`). Expect a
197
+ binary over 100 MB: the Bun runtime plus about 120 admin assets (fonts, images, JS).
198
+
199
+ ### 7. Verify the binary on its own
200
+
201
+ Copy the binary to an empty directory (or move `node_modules` aside) so nothing resolves from
202
+ disk, then:
203
+
204
+ ```bash
205
+ MONGO_URI=... BETTER_AUTH_SECRET=... PUBLIC_API_URL=http://localhost:4000 ./dist/<appName> &
206
+ curl -sf localhost:4000/health
207
+ curl -sf -o /dev/null -w '%{http_code}\n' localhost:4000/console # 200
208
+ curl -sf localhost:4000/console/app-config.json
209
+ curl -sf -o /dev/null -w '%{http_code}\n' localhost:4000/console/_expo/static/js/web/<entry>.js # 200
210
+ curl -s -X POST localhost:4000/mcp -H 'content-type: application/json' \
211
+ -H "authorization: Bearer mcp_..." \
212
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
213
+ ```
214
+
215
+ Seed an admin, log in at `/console`, mint a service token with `POST /mcp/service-tokens`, run
216
+ `tools/list` with it, and confirm a row appears in **MCP request log** after a `tools/call`. Save a screenshot of the MCP admin
217
+ group to `/opt/cursor/artifacts/` when that directory exists.
218
+
219
+ ### 8. Expose it
220
+
221
+ Never put `localhost:4000` on the internet by port-forwarding. Follow
222
+ https://terreno-docs.netlify.app/how-to/expose-mcp-server-safely: a named Cloudflare Tunnel, Cloudflare Access on
223
+ `/admin`, plus `/console` and `/api/auth` when every OAuth approver is in the Access policy
224
+ (never `/mcp`, `/.well-known`, or `/oauth/*`), OAuth `issuer`
225
+ equal to the tunnel hostname, rate limiting on with `trustProxy` set for the proxy in front.
226
+
227
+ ## Done when
228
+
229
+ - `./dist/<appName>` starts with only Mongo and env vars, and the step 7 checks pass.
230
+ - `/console` shows the MCP admin group, and a service token minted there lists tools.
231
+ - With `MCP_OAUTH_ENABLED=true`, Claude.ai's custom connector completes the consent flow
232
+ against the public hostname.
233
+
234
+ ## Pitfalls
235
+
236
+ - A quick tunnel (`trycloudflare.com`) hostname changes on every run. OAuth discovery and
237
+ registered clients break; use a named tunnel.
238
+ - `dynamicClientRegistration` with an empty `allowedRedirectHosts` throws at startup on purpose.
239
+ - Non-admin users can open `/console/oauth/consent` to approve a connection; the rest of the
240
+ console shows them the forbidden page. That is expected.
241
+ - Run one binary per Mongo database.
242
+ - The OAuth consent routes trust the Better Auth session cookie because `BetterAuthApp`
243
+ mounts its session middleware before `TerrenoApp` adds `/oauth/*`. Register `BetterAuthApp`
244
+ on the same `TerrenoApp`; do not mount it on a separate Express app.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: building-admin-interfaces
3
+ description: >-
4
+ Build Terreno admin UIs with @terreno/admin-frontend and @terreno/admin-backend.
5
+ Use when adding or changing admin screens, AdminShell sidebar nav, AdminHome,
6
+ custom screens, AdminScreenRouter, AdminProvider widgets, admin-spa routes,
7
+ or operator tools. Covers where models vs custom screens vs platform tools go.
8
+ Not for product (non-admin) Expo screens — use building-terreno-apps.
9
+ ---
10
+ # Building Admin Interfaces
11
+
12
+ Use this skill when the work is an **admin** surface: `/admin/**`, `/console`,
13
+ `AdminApp`, `AdminShellLayout`, model changelists, custom operator screens, or
14
+ admin home widgets.
15
+
16
+ **Architecture source (read first):**
17
+ [How admin interfaces are shaped](../../../../docs/explanation/admin-interface.md)
18
+ and [Build admin screens](../../../../docs/how-to/build-admin-screens.md).
19
+
20
+ **Related skills:** `building-terreno-apps` (Expo/`@terreno/ui` habits),
21
+ `terreno-ui` (components), `terreno-data-fetching` (windowed syncdb + RPC),
22
+ `terreno-backend-api` (`modelRouter` + `admin:`), `verify-ui-changes`.
23
+
24
+ ## Tracer bullet
25
+
26
+ 1. Classify the need: model CRUD, home widget, custom screen, or dedicated workflow
27
+ route. Done when the choice matches the table in `docs/how-to/build-admin-screens.md`.
28
+ 2. Backend first: `modelRouter({admin})`, `AdminApp.customScreens`, plugin
29
+ `adminContribution()`, or `home.slots`. Done when `GET /admin/config` lists the
30
+ new model or screen for an admin user and omits it without `admin:access`.
31
+ 3. Frontend wiring: `AdminProvider` + `AdminShellLayout` with matching `apiBase` /
32
+ `routeBase`. Register `widgets.screens[name]` only for host-owned custom screens.
33
+ Done when the sidebar link opens the right component without a missing-widget
34
+ placeholder.
35
+ 4. Routes: keep `[model]/index.tsx` on `AdminScreenRouter`; `create.tsx` / `[id].tsx`
36
+ on `AdminModelForm`. Add extra Expo files only for extra URL segments. Done when
37
+ no per-model copy of the table exists.
38
+ 5. UI: `@terreno/ui` + `AdminScreenPage` for custom screens; `color="transparent"`
39
+ inside the shell. String-`_id` models with `adminBroadcast` use windowed
40
+ syncdb; ObjectId/API-only model CRUD uses `useAdminModelApi`; custom RPC uses
41
+ `useAdminRpc` + `useAdminRpcQuery`. All of it goes through the `AdminProvider`
42
+ fetch client. Done when there is no app-owned `fetch`/`axios` and no `api`
43
+ prop on admin components.
44
+ 6. Verify with `verify-ui-changes` on the example admin or admin-spa. Done when
45
+ home, sidebar, and the new flow are exercised as an admin user.
46
+
47
+ ## References
48
+
49
+ ```
50
+ references/
51
+ screens-and-routes.md File layout, AdminScreenRouter, dedicated routes
52
+ nav-and-shell.md Sidebar sections, routeBase vs apiBase, mobile drawer
53
+ patterns-and-tools.md Widgets, platform tools, RBAC, MCP, built-in IDs
54
+ ```
55
+
56
+ ## Hard guardrails
57
+
58
+ - Do not hand-roll a DataTable per model. Use `AdminModelTable` / `AdminScreenRouter`.
59
+ - Do not put a second sidebar inside a screen. `AdminShell` owns nav.
60
+ - Do not pass an `api` prop to admin components (removed in Terreno 58).
61
+ `AdminProvider` (`credentials` for cookie SPAs, `apiOrigin` = `baseUrl` from
62
+ `@terreno/syncdb/config` + `getAuthHeaders` reading the app's
63
+ `authProvider.getToken()` for embedded apps) is the only auth binding.
64
+ - Do not invent sidebar entries. Config (filtered by RBAC) is the nav source.
65
+ - Do not use `router.back()` as the custom-screen back action; use `AdminScreenPage`.
66
+ - Do not edit generated `store/apiSdk.ts` / `store/syncDbSdk.ts` by hand.
67
+
68
+ ## Decision tree
69
+
70
+ ```
71
+ Admin work?
72
+ |-- CRUD for a model?
73
+ | \-- modelRouter admin block; generic [model] routes
74
+ |
75
+ |-- Tile on Home?
76
+ | \-- home.slots + widgets.home
77
+ |
78
+ |-- Operator page that is not a model?
79
+ | \-- customScreens.name + widgets.screens[name]
80
+ |
81
+ |-- Extra URL segments or a workflow form cannot express?
82
+ | \-- Dedicated Expo route; keep the generic table for search
83
+ |
84
+ \-- Done coding?
85
+ \-- verify-ui-changes
86
+ ```
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Building Admin Interfaces"
3
+ short_description: "Build Terreno admin screens with AdminShell nav, AdminScreenRouter, custom screens, and admin-backend config"
4
+ default_prompt: "Use $building-admin-interfaces to add or change an admin screen, sidebar nav, or operator tool with @terreno/admin-frontend."
@@ -0,0 +1,42 @@
1
+ # Nav and shell
2
+
3
+ Canonical shape: [How admin interfaces are shaped](../../../../../docs/explanation/admin-interface.md).
4
+
5
+ ## `apiBase` vs `routeBase`
6
+
7
+ | Prop | Meaning | Embedded | admin-spa |
8
+ | --- | --- | --- | --- |
9
+ | `apiBase` | HTTP prefix for `/config` and model CRUD | `"/admin"` | `"/admin"` |
10
+ | `routeBase` | Expo Router prefix for `router.push` | `"/admin"` | `""` |
11
+
12
+ `AdminShell` navigates to `` `${routeBase}/${model.name}` ``. Using `apiBase` as
13
+ `routeBase` on the SPA sends the browser to `/admin/User` instead of `/console/User`.
14
+
15
+ ## Sidebar sections (do not reorder in app code)
16
+
17
+ 1. Home → `{routeBase}/`
18
+ 2. Models, grouped by `admin.group` (empty group → **General**, sorted last)
19
+ 3. Screens from `customScreens`
20
+ 4. Platform, filtered by `platformTools`: Scripts (`/__scripts`), Roles, Version,
21
+ Audit Log, Feature Flags, Configuration
22
+
23
+ Pass `configurationPath`, `rolesPath`, and `versionConfigPath` on
24
+ `AdminShellLayout`. Audit log and Feature Flags are still models; the shell
25
+ relocates those two into Platform.
26
+
27
+ ## Chrome
28
+
29
+ - Default sidebar: `sidebarVariant="colorful"` (teal rail). Optional `"clinical"`.
30
+ - Main column: theme canvas. Nested `Page` / `AdminScreenPage`:
31
+ `color="transparent"`, `padding={0}` unless the screen supplies its own pad.
32
+ - Breadcrumbs and `headerActions` enable the white top bar. Home typically uses
33
+ `breadcrumbs={[{label: "Admin"}]}`.
34
+ - Below the desktop floor (1024pt, `isSupportedDesktopWidth`): hamburger + drawer. Do not hide the shell on mobile.
35
+ - Custom-screen back: `AdminScreenPage` → `router.push(backHref)`. Never
36
+ `router.back()` as the default.
37
+
38
+ ## Auth gate
39
+
40
+ `admin:access` opens the page (`GET /admin/config` 403 without it). Hosts call
41
+ `canOpenAdminPage({admin, permissions})` from `@terreno/syncdb/app` and hide product
42
+ nav to admin with the same check.