@terreno/mcp 57.3.0 → 57.4.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 +7 -1
- package/dist/bootstrap.d.ts +0 -18
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +10 -2428
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/dist/docs/resources/syncdb.md +4 -1
- package/dist/docs/upgrades/57.4.0.md +140 -0
- package/dist/docs/versioned/next/README.md +3 -0
- package/dist/docs/versioned/next/explanation/README.md +4 -1
- package/dist/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/dist/docs/versioned/next/explanation/charts.md +29 -0
- package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/dist/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/dist/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/dist/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/dist/docs/versioned/next/explanation/organizations.md +88 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/dist/docs/versioned/next/explanation/positioning.md +3 -3
- package/dist/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/dist/docs/versioned/next/explanation/source-rules.md +1 -1
- package/dist/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/dist/docs/versioned/next/how-to/README.md +10 -0
- package/dist/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/dist/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/dist/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/dist/docs/versioned/next/how-to/audit-log.md +85 -0
- package/dist/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/dist/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/dist/docs/versioned/next/how-to/circleci.md +136 -27
- package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/dist/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/dist/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/dist/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/dist/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/dist/docs/versioned/next/implementationPlans/README.md +8 -5
- package/dist/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/dist/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/dist/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/dist/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/dist/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/dist/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/dist/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/dist/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/dist/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/dist/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/dist/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/dist/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/dist/docs/versioned/next/reference/README.md +4 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +83 -5
- package/dist/docs/versioned/next/reference/admin-config.md +7 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/dist/docs/versioned/next/reference/admin-spa.md +18 -1
- package/dist/docs/versioned/next/reference/ai.md +2 -0
- package/dist/docs/versioned/next/reference/announcements.md +179 -0
- package/dist/docs/versioned/next/reference/api.md +343 -4
- package/dist/docs/versioned/next/reference/comms.md +33 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +29 -0
- package/dist/docs/versioned/next/reference/jobs.md +319 -0
- package/dist/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/dist/docs/versioned/next/reference/mcp-server.md +2 -43
- package/dist/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/dist/docs/versioned/next/reference/syncdb.md +38 -12
- package/dist/docs/versioned/next/reference/test.md +57 -6
- package/dist/docs/versioned/next/reference/ui-performance.md +2 -1
- package/dist/docs/versioned/next/reference/ui.md +257 -3
- package/dist/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/dist/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/dist/docs/versioned/next/tasks/announcements.md +173 -0
- package/dist/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/dist/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/dist/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/dist/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/dist/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/dist/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/dist/docs/versioned/next/tasks/job-queues.md +144 -0
- package/dist/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/dist/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/dist/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/dist/docs/versioned/next/tasks/notification-center.md +102 -0
- package/dist/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/dist/docs/versioned/next/tutorials/README.md +1 -1
- package/dist/docs/versioned/next/tutorials/getting-started.md +62 -22
- package/dist/help/excerpt.d.ts +2 -0
- package/dist/help/excerpt.d.ts.map +1 -0
- package/dist/help/excerpt.js +8 -0
- package/dist/help/excerpt.js.map +1 -0
- package/dist/help/updateNotes.d.ts +42 -0
- package/dist/help/updateNotes.d.ts.map +1 -0
- package/dist/help/updateNotes.js +235 -0
- package/dist/help/updateNotes.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts +1 -0
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +1 -0
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/packageGuidelines.d.ts +2 -2
- package/dist/packageGuidelines.d.ts.map +1 -1
- package/dist/packageGuidelines.js +2 -8
- package/dist/packageGuidelines.js.map +1 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +3 -0
- package/dist/resources.js.map +1 -1
- package/dist/search/chunker.d.ts +1 -0
- package/dist/search/chunker.d.ts.map +1 -1
- package/dist/search/chunker.js +1 -0
- package/dist/search/chunker.js.map +1 -1
- package/dist/search/docIndex.d.ts +1 -5
- package/dist/search/docIndex.d.ts.map +1 -1
- package/dist/search/docIndex.js +1 -0
- package/dist/search/docIndex.js.map +1 -1
- package/dist/search/docVersion.d.ts +2 -1
- package/dist/search/docVersion.d.ts.map +1 -1
- package/dist/search/docVersion.js +3 -1
- package/dist/search/docVersion.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +113 -2
- package/dist/tools.js.map +1 -1
- package/package.json +28 -27
- package/src/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/src/docs/resources/syncdb.md +4 -1
- package/src/docs/upgrades/57.4.0.md +140 -0
- package/src/docs/versioned/next/README.md +3 -0
- package/src/docs/versioned/next/explanation/README.md +4 -1
- package/src/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/src/docs/versioned/next/explanation/charts.md +29 -0
- package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/src/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/src/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/src/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/src/docs/versioned/next/explanation/organizations.md +88 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/src/docs/versioned/next/explanation/positioning.md +3 -3
- package/src/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/src/docs/versioned/next/explanation/source-rules.md +1 -1
- package/src/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/src/docs/versioned/next/how-to/README.md +10 -0
- package/src/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/src/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/src/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/src/docs/versioned/next/how-to/audit-log.md +85 -0
- package/src/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/src/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/src/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/src/docs/versioned/next/how-to/circleci.md +136 -27
- package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/src/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/src/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/src/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/src/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/src/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/src/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/src/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/src/docs/versioned/next/implementationPlans/README.md +8 -5
- package/src/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/src/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/src/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/src/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/src/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/src/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/src/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/src/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/src/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/src/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/src/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/src/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/src/docs/versioned/next/reference/README.md +4 -0
- package/src/docs/versioned/next/reference/admin-backend.md +83 -5
- package/src/docs/versioned/next/reference/admin-config.md +7 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/src/docs/versioned/next/reference/admin-spa.md +18 -1
- package/src/docs/versioned/next/reference/ai.md +2 -0
- package/src/docs/versioned/next/reference/announcements.md +179 -0
- package/src/docs/versioned/next/reference/api.md +343 -4
- package/src/docs/versioned/next/reference/comms.md +33 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/src/docs/versioned/next/reference/environment-variables.md +29 -0
- package/src/docs/versioned/next/reference/jobs.md +319 -0
- package/src/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/src/docs/versioned/next/reference/mcp-server.md +2 -43
- package/src/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/src/docs/versioned/next/reference/syncdb.md +38 -12
- package/src/docs/versioned/next/reference/test.md +57 -6
- package/src/docs/versioned/next/reference/ui-performance.md +2 -1
- package/src/docs/versioned/next/reference/ui.md +257 -3
- package/src/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/src/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/src/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/src/docs/versioned/next/tasks/announcements.md +173 -0
- package/src/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/src/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/src/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/src/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/src/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/src/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/src/docs/versioned/next/tasks/job-queues.md +144 -0
- package/src/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/src/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/src/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/src/docs/versioned/next/tasks/notification-center.md +102 -0
- package/src/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/src/docs/versioned/next/tutorials/README.md +1 -1
- package/src/docs/versioned/next/tutorials/getting-started.md +62 -22
package/dist/bootstrap.js
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import { composePackageGuidelinesForRules, filterGuidelineIdsForRootRules, loadPackageGuidelineMarkdown, resolveBootstrapGuidelinePackages, } from "./packageGuidelines.js";
|
|
2
|
-
/** Pinned so `npx -y` does not resolve a moving `@latest` target in generated MCP configs. */
|
|
3
|
-
export const PLAYWRIGHT_MCP_PACKAGE_VERSION = "0.0.76";
|
|
4
2
|
export const bootstrapTools = [
|
|
5
|
-
{
|
|
6
|
-
description: "Bootstrap a new Terreno full-stack application with frontend (Expo/React Native) and backend (Express/Mongoose) directories, including Cursor rules and MCP settings",
|
|
7
|
-
inputSchema: {
|
|
8
|
-
properties: {
|
|
9
|
-
appDisplayName: {
|
|
10
|
-
description: "Human-readable display name (e.g., 'My Todo App', 'Task Manager')",
|
|
11
|
-
type: "string",
|
|
12
|
-
},
|
|
13
|
-
appName: {
|
|
14
|
-
description: "The application name in kebab-case (e.g., 'my-app', 'todo-app'). Used for directory names and package names.",
|
|
15
|
-
type: "string",
|
|
16
|
-
},
|
|
17
|
-
description: {
|
|
18
|
-
description: "A brief description of the app (optional)",
|
|
19
|
-
type: "string",
|
|
20
|
-
},
|
|
21
|
-
mcpServerUrl: {
|
|
22
|
-
default: "https://mcp.terreno.flourish.health",
|
|
23
|
-
description: "URL of the Terreno MCP server for AI assistance",
|
|
24
|
-
type: "string",
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
required: ["appName", "appDisplayName"],
|
|
28
|
-
type: "object",
|
|
29
|
-
},
|
|
30
|
-
name: "terreno_bootstrap_app",
|
|
31
|
-
},
|
|
32
3
|
{
|
|
33
4
|
description: "Bootstrap AI coding assistant rules for a Terreno project. Creates configuration files for Cursor, Windsurf, Claude Code, and GitHub Copilot with Terreno-specific guidelines adapted for the project.",
|
|
34
5
|
inputSchema: {
|
|
@@ -57,2156 +28,6 @@ export const bootstrapTools = [
|
|
|
57
28
|
name: "terreno_bootstrap_ai_rules",
|
|
58
29
|
},
|
|
59
30
|
];
|
|
60
|
-
const _toPascalCase = (str) => {
|
|
61
|
-
return str
|
|
62
|
-
.split("-")
|
|
63
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
64
|
-
.join("");
|
|
65
|
-
};
|
|
66
|
-
const generateCursorRules = (args) => {
|
|
67
|
-
const { appDisplayName } = args;
|
|
68
|
-
return `# ${appDisplayName}
|
|
69
|
-
|
|
70
|
-
A full-stack application built with the Terreno framework.
|
|
71
|
-
|
|
72
|
-
## Project Structure
|
|
73
|
-
|
|
74
|
-
- **frontend/** - Expo/React Native frontend using @terreno/ui, @terreno/syncdb, and @terreno/rtk (SDK + Better Auth)
|
|
75
|
-
- **backend/** - Express/Mongoose backend using @terreno/api
|
|
76
|
-
|
|
77
|
-
## Development
|
|
78
|
-
|
|
79
|
-
Uses [Bun](https://bun.sh/) as the package manager.
|
|
80
|
-
|
|
81
|
-
\`\`\`bash
|
|
82
|
-
# Backend
|
|
83
|
-
cd backend && bun run dev # Start backend on port 4000
|
|
84
|
-
|
|
85
|
-
# Frontend
|
|
86
|
-
cd frontend && bun run web # Start web frontend
|
|
87
|
-
cd frontend && bun run sdk # Regenerate SDK after backend changes
|
|
88
|
-
\`\`\`
|
|
89
|
-
|
|
90
|
-
## Code Style
|
|
91
|
-
|
|
92
|
-
### TypeScript/JavaScript
|
|
93
|
-
- Use ES module syntax and TypeScript for all code
|
|
94
|
-
- Prefer interfaces over types; avoid enums, use maps
|
|
95
|
-
- Prefer const arrow functions over \`function\` keyword
|
|
96
|
-
- Use descriptive variable names with auxiliary verbs (e.g., \`isLoading\`)
|
|
97
|
-
- Favor named exports
|
|
98
|
-
- Use the RORO pattern (Receive an Object, Return an Object)
|
|
99
|
-
|
|
100
|
-
### Dates and Time
|
|
101
|
-
- Always use Luxon instead of Date or dayjs
|
|
102
|
-
|
|
103
|
-
### Error Handling
|
|
104
|
-
- Check error conditions at start of functions and return early
|
|
105
|
-
- Limit nested if statements
|
|
106
|
-
- Use multiline syntax with curly braces for all conditionals
|
|
107
|
-
|
|
108
|
-
### React Best Practices
|
|
109
|
-
- Use functional components with \`React.FC\` type
|
|
110
|
-
- Add explanatory comment above each \`useEffect\`
|
|
111
|
-
- Wrap callbacks in \`useCallback\`
|
|
112
|
-
- Use Redux Toolkit for state management
|
|
113
|
-
|
|
114
|
-
### Backend Conventions
|
|
115
|
-
- Use \`modelRouter\` for CRUD endpoints
|
|
116
|
-
- Use \`APIError\` for error responses
|
|
117
|
-
- Use \`logger.info/warn/error/debug\` for logging
|
|
118
|
-
- Use \`Model.findExactlyOne\` or \`Model.findOneOrNone\` (not \`Model.findOne\`)
|
|
119
|
-
|
|
120
|
-
### Frontend Conventions
|
|
121
|
-
- Use \`@terreno/syncdb/react\` hooks (\`useQuery\`, \`useMutate\`) for synced collection CRUD
|
|
122
|
-
- Use generated SDK hooks from \`@/store/openApiSdk\` for non-synced routes only
|
|
123
|
-
- Use @terreno/ui components (Box, Page, Button, TextField, etc.)
|
|
124
|
-
- Never modify \`openApiSdk.ts\` manually - regenerate with \`bun run sdk\`
|
|
125
|
-
|
|
126
|
-
## AI Assistance
|
|
127
|
-
|
|
128
|
-
This project is configured to use the Terreno MCP server for AI-assisted development.
|
|
129
|
-
Use the MCP tools and prompts for:
|
|
130
|
-
- Generating new models and routes
|
|
131
|
-
- Creating screens and forms
|
|
132
|
-
- Following Terreno patterns and best practices
|
|
133
|
-
`;
|
|
134
|
-
};
|
|
135
|
-
const generateMcpSettings = (args) => {
|
|
136
|
-
const mcpUrl = args.mcpServerUrl || "https://mcp.terreno.flourish.health";
|
|
137
|
-
return JSON.stringify({
|
|
138
|
-
mcpServers: {
|
|
139
|
-
expo: {
|
|
140
|
-
type: "http",
|
|
141
|
-
url: "https://mcp.expo.dev/mcp",
|
|
142
|
-
},
|
|
143
|
-
playwright: {
|
|
144
|
-
args: ["-y", `@playwright/mcp@${PLAYWRIGHT_MCP_PACKAGE_VERSION}`],
|
|
145
|
-
command: "npx",
|
|
146
|
-
},
|
|
147
|
-
terreno: {
|
|
148
|
-
type: "http",
|
|
149
|
-
url: `${mcpUrl}/mcp`,
|
|
150
|
-
},
|
|
151
|
-
"terreno-local": {
|
|
152
|
-
args: ["terreno-mcp-local"],
|
|
153
|
-
command: "bunx",
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
}, null, 2);
|
|
157
|
-
};
|
|
158
|
-
const generateRootGitignore = () => {
|
|
159
|
-
return [
|
|
160
|
-
"# Dependencies",
|
|
161
|
-
"node_modules/",
|
|
162
|
-
"",
|
|
163
|
-
"# Local Terreno dev logs (winston JSONL + optional browser fallback)",
|
|
164
|
-
".terreno/",
|
|
165
|
-
"",
|
|
166
|
-
"# Build outputs",
|
|
167
|
-
"dist/",
|
|
168
|
-
"build/",
|
|
169
|
-
"",
|
|
170
|
-
"# OS",
|
|
171
|
-
".DS_Store",
|
|
172
|
-
"",
|
|
173
|
-
].join("\n");
|
|
174
|
-
};
|
|
175
|
-
const generateClaudeMd = (args) => {
|
|
176
|
-
const { appDisplayName } = args;
|
|
177
|
-
return `# ${appDisplayName}
|
|
178
|
-
|
|
179
|
-
A full-stack application built with the Terreno framework.
|
|
180
|
-
|
|
181
|
-
## Project Structure
|
|
182
|
-
|
|
183
|
-
- **frontend/** - Expo/React Native frontend
|
|
184
|
-
- **backend/** - Express/Mongoose backend
|
|
185
|
-
|
|
186
|
-
## Development
|
|
187
|
-
|
|
188
|
-
\`\`\`bash
|
|
189
|
-
# Install dependencies
|
|
190
|
-
cd backend && bun install
|
|
191
|
-
cd frontend && bun install
|
|
192
|
-
|
|
193
|
-
# Start backend (port 4000)
|
|
194
|
-
cd backend && bun run dev
|
|
195
|
-
|
|
196
|
-
# Start frontend (port 8082)
|
|
197
|
-
cd frontend && bun run web
|
|
198
|
-
|
|
199
|
-
# Regenerate SDK after backend changes
|
|
200
|
-
cd frontend && bun run sdk
|
|
201
|
-
\`\`\`
|
|
202
|
-
|
|
203
|
-
## Adding Features
|
|
204
|
-
|
|
205
|
-
1. Create model in \`backend/src/models/\`
|
|
206
|
-
2. Create route in \`backend/src/api/\`
|
|
207
|
-
3. Register route in \`backend/src/server.ts\`
|
|
208
|
-
4. Regenerate SDK: \`cd frontend && bun run sdk\`
|
|
209
|
-
5. Create screens in \`frontend/app/\`
|
|
210
|
-
|
|
211
|
-
## Code Style
|
|
212
|
-
|
|
213
|
-
- Use TypeScript with ES modules
|
|
214
|
-
- Use Luxon for dates
|
|
215
|
-
- Prefer const arrow functions
|
|
216
|
-
- Named exports preferred
|
|
217
|
-
- Use interfaces over types
|
|
218
|
-
`;
|
|
219
|
-
};
|
|
220
|
-
// Backend file generators
|
|
221
|
-
const generateBackendPackageJson = (args) => {
|
|
222
|
-
const { appName } = args;
|
|
223
|
-
return JSON.stringify({
|
|
224
|
-
dependencies: {
|
|
225
|
-
"@terreno/admin-backend": "latest",
|
|
226
|
-
"@terreno/api": "latest",
|
|
227
|
-
luxon: "^3.7.2",
|
|
228
|
-
mongoose: "^9.7.4",
|
|
229
|
-
"passport-local-mongoose": "^9.0.1",
|
|
230
|
-
},
|
|
231
|
-
devDependencies: {
|
|
232
|
-
"@biomejs/biome": "^2.3.6",
|
|
233
|
-
"@types/bun": "^1.4.0",
|
|
234
|
-
"@types/express": "^4.17.21",
|
|
235
|
-
"@types/luxon": "^3.7.1",
|
|
236
|
-
typescript: "~5.9.2",
|
|
237
|
-
},
|
|
238
|
-
name: `@${appName}/backend`,
|
|
239
|
-
private: true,
|
|
240
|
-
scripts: {
|
|
241
|
-
build: "bun build src/index.ts --outdir ./dist --target node",
|
|
242
|
-
compile: "tsc",
|
|
243
|
-
dev: "PORT=4000 bun run --watch src/index.ts",
|
|
244
|
-
format: "biome format --write .",
|
|
245
|
-
lint: "biome check .",
|
|
246
|
-
"lint:fix": "biome check --write .",
|
|
247
|
-
"lint:unsafefix": "biome check --write . --unsafe",
|
|
248
|
-
seed: "bun run src/scripts/seed.ts",
|
|
249
|
-
start: "bun run src/index.ts",
|
|
250
|
-
test: "bun test",
|
|
251
|
-
},
|
|
252
|
-
type: "module",
|
|
253
|
-
version: "1.0.0",
|
|
254
|
-
}, null, 2);
|
|
255
|
-
};
|
|
256
|
-
const generateBackendTsConfig = () => {
|
|
257
|
-
return JSON.stringify({
|
|
258
|
-
compilerOptions: {
|
|
259
|
-
allowSyntheticDefaultImports: true,
|
|
260
|
-
esModuleInterop: true,
|
|
261
|
-
lib: ["ESNext"],
|
|
262
|
-
module: "ESNext",
|
|
263
|
-
moduleResolution: "bundler",
|
|
264
|
-
noEmit: true,
|
|
265
|
-
outDir: "./dist",
|
|
266
|
-
resolveJsonModule: true,
|
|
267
|
-
skipLibCheck: true,
|
|
268
|
-
strict: true,
|
|
269
|
-
target: "ESNext",
|
|
270
|
-
types: ["bun-types"],
|
|
271
|
-
},
|
|
272
|
-
exclude: ["node_modules", "dist"],
|
|
273
|
-
include: ["src/**/*"],
|
|
274
|
-
}, null, 2);
|
|
275
|
-
};
|
|
276
|
-
const generateBackendBiomeJsonc = () => {
|
|
277
|
-
return `{
|
|
278
|
-
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
279
|
-
"assist": {
|
|
280
|
-
"actions": {
|
|
281
|
-
"source": {
|
|
282
|
-
"organizeImports": "on",
|
|
283
|
-
"useSortedAttributes": "on",
|
|
284
|
-
"useSortedKeys": "on"
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
"enabled": true
|
|
288
|
-
},
|
|
289
|
-
"files": {
|
|
290
|
-
"includes": [
|
|
291
|
-
"package.json",
|
|
292
|
-
"src/**/*.ts",
|
|
293
|
-
"src/**/*.tsx",
|
|
294
|
-
"!!**/node_modules",
|
|
295
|
-
"!!**/dist",
|
|
296
|
-
"!!**/build",
|
|
297
|
-
"!!**/coverage",
|
|
298
|
-
"!!**/.git"
|
|
299
|
-
]
|
|
300
|
-
},
|
|
301
|
-
"formatter": {
|
|
302
|
-
"enabled": true,
|
|
303
|
-
"indentStyle": "space",
|
|
304
|
-
"indentWidth": 2,
|
|
305
|
-
"lineWidth": 100
|
|
306
|
-
},
|
|
307
|
-
"javascript": {
|
|
308
|
-
"formatter": {
|
|
309
|
-
"arrowParentheses": "always",
|
|
310
|
-
"bracketSpacing": false,
|
|
311
|
-
"jsxQuoteStyle": "double",
|
|
312
|
-
"quoteStyle": "double",
|
|
313
|
-
"semicolons": "always",
|
|
314
|
-
"trailingCommas": "es5"
|
|
315
|
-
},
|
|
316
|
-
// TODO: Remove once we don't need to import React from 'react' in our JSX files.
|
|
317
|
-
"jsxRuntime": "reactClassic"
|
|
318
|
-
},
|
|
319
|
-
"linter": {
|
|
320
|
-
"enabled": true,
|
|
321
|
-
"rules": {
|
|
322
|
-
"nursery": {
|
|
323
|
-
"noFloatingPromises": "error",
|
|
324
|
-
"noMisusedPromises": "error",
|
|
325
|
-
"useExhaustiveSwitchCases": "error"
|
|
326
|
-
},
|
|
327
|
-
"recommended": true,
|
|
328
|
-
"suspicious": {
|
|
329
|
-
"noConsole": {
|
|
330
|
-
"level": "error",
|
|
331
|
-
"options": {
|
|
332
|
-
"allow": ["assert", "debug", "error", "info", "warn"]
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
"noExplicitAny": "off",
|
|
336
|
-
"noImplicitAnyLet": "off"
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
"vcs": {
|
|
341
|
-
"clientKind": "git",
|
|
342
|
-
"defaultBranch": "master",
|
|
343
|
-
"enabled": true
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
`;
|
|
347
|
-
};
|
|
348
|
-
const generateBackendIndex = () => {
|
|
349
|
-
return `import "./server";
|
|
350
|
-
`;
|
|
351
|
-
};
|
|
352
|
-
const generateBackendServer = (args) => {
|
|
353
|
-
const { appDisplayName } = args;
|
|
354
|
-
return `import {AdminApp} from "@terreno/admin-backend";
|
|
355
|
-
import {
|
|
356
|
-
BetterAuthApp,
|
|
357
|
-
checkModelsStrict,
|
|
358
|
-
createBetterAuth,
|
|
359
|
-
getMongoClientFromMongoose,
|
|
360
|
-
logger,
|
|
361
|
-
RealtimeApp,
|
|
362
|
-
SyncApp,
|
|
363
|
-
TerrenoApp,
|
|
364
|
-
type UserModel as TerrenoAuthUserModel,
|
|
365
|
-
} from "@terreno/api";
|
|
366
|
-
import type express from "express";
|
|
367
|
-
import {userRouter} from "./api/users";
|
|
368
|
-
import {AppConfiguration} from "./models/appConfiguration";
|
|
369
|
-
import {User} from "./models/user";
|
|
370
|
-
import {buildBetterAuthConfig, getWebOrigins} from "./utils/betterAuthConfig";
|
|
371
|
-
import {connectToMongoDB} from "./utils/database";
|
|
372
|
-
|
|
373
|
-
const isDeployed = process.env.NODE_ENV === "production";
|
|
374
|
-
|
|
375
|
-
export async function start(skipListen = false): Promise<express.Application> {
|
|
376
|
-
await connectToMongoDB();
|
|
377
|
-
|
|
378
|
-
logger.info(\`Starting ${appDisplayName} server on port \${process.env.PORT || 4000}\`);
|
|
379
|
-
|
|
380
|
-
if (!isDeployed) {
|
|
381
|
-
checkModelsStrict();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const betterAuthConfig = buildBetterAuthConfig();
|
|
385
|
-
const betterAuthInstance = betterAuthConfig
|
|
386
|
-
? createBetterAuth({
|
|
387
|
-
config: betterAuthConfig,
|
|
388
|
-
mongoClient: getMongoClientFromMongoose(),
|
|
389
|
-
// noExplicitAny: User model type mismatch
|
|
390
|
-
// biome-ignore lint/suspicious/noExplicitAny: User model type mismatch
|
|
391
|
-
userModel: User as any,
|
|
392
|
-
})
|
|
393
|
-
: undefined;
|
|
394
|
-
|
|
395
|
-
const app = new TerrenoApp({
|
|
396
|
-
corsOrigin: getWebOrigins(),
|
|
397
|
-
loggingOptions: {
|
|
398
|
-
disableConsoleColors: isDeployed,
|
|
399
|
-
level: "debug",
|
|
400
|
-
logRequests: !isDeployed,
|
|
401
|
-
},
|
|
402
|
-
skipListen,
|
|
403
|
-
// noExplicitAny: User model type mismatch
|
|
404
|
-
// biome-ignore lint/suspicious/noExplicitAny: User model type mismatch
|
|
405
|
-
userModel: User as any,
|
|
406
|
-
}).configure(AppConfiguration);
|
|
407
|
-
|
|
408
|
-
if (betterAuthConfig) {
|
|
409
|
-
app.register(
|
|
410
|
-
new BetterAuthApp({
|
|
411
|
-
config: betterAuthConfig,
|
|
412
|
-
userModel: User as unknown as TerrenoAuthUserModel,
|
|
413
|
-
})
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
app
|
|
418
|
-
.register(userRouter)
|
|
419
|
-
.register(
|
|
420
|
-
new AdminApp({
|
|
421
|
-
models: [
|
|
422
|
-
{
|
|
423
|
-
displayName: "Users",
|
|
424
|
-
listFields: ["email", "name", "admin", "created"],
|
|
425
|
-
// noExplicitAny: User model type mismatch
|
|
426
|
-
// biome-ignore lint/suspicious/noExplicitAny: User model type mismatch
|
|
427
|
-
model: User as any,
|
|
428
|
-
routePath: "/users",
|
|
429
|
-
},
|
|
430
|
-
],
|
|
431
|
-
})
|
|
432
|
-
)
|
|
433
|
-
.register(new SyncApp())
|
|
434
|
-
.register(
|
|
435
|
-
new RealtimeApp({
|
|
436
|
-
betterAuth: betterAuthInstance
|
|
437
|
-
? {
|
|
438
|
-
auth: betterAuthInstance,
|
|
439
|
-
// noExplicitAny: User model type mismatch
|
|
440
|
-
// biome-ignore lint/suspicious/noExplicitAny: User model type mismatch
|
|
441
|
-
userModel: User as any,
|
|
442
|
-
}
|
|
443
|
-
: undefined,
|
|
444
|
-
// noExplicitAny: User model type mismatch
|
|
445
|
-
// biome-ignore lint/suspicious/noExplicitAny: User model type mismatch
|
|
446
|
-
userModel: User as any,
|
|
447
|
-
})
|
|
448
|
-
)
|
|
449
|
-
.start();
|
|
450
|
-
|
|
451
|
-
return app;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
start().catch((error) => {
|
|
455
|
-
logger.error(\`Fatal error starting server: \${error}\`);
|
|
456
|
-
});
|
|
457
|
-
`;
|
|
458
|
-
};
|
|
459
|
-
const generateBackendDatabase = (args) => {
|
|
460
|
-
const { appName } = args;
|
|
461
|
-
const dbName = appName.replace(/-/g, "_");
|
|
462
|
-
return `import {logger} from "@terreno/api";
|
|
463
|
-
import mongoose from "mongoose";
|
|
464
|
-
|
|
465
|
-
export const connectToMongoDB = async (): Promise<void> => {
|
|
466
|
-
if (mongoose.connection.readyState === 1) {
|
|
467
|
-
logger.info("Already connected to MongoDB");
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const mongoURI =
|
|
472
|
-
process.env.MONGO_URI || "mongodb://127.0.0.1:27017/${dbName}?replicaSet=rs0";
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
await mongoose.connect(mongoURI);
|
|
476
|
-
logger.info("Connected to MongoDB");
|
|
477
|
-
} catch (error: unknown) {
|
|
478
|
-
logger.error(\`MongoDB connection error: \${error}\`);
|
|
479
|
-
throw error;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
mongoose.connection.on("error", (error: unknown) => {
|
|
483
|
-
logger.error(\`MongoDB connection error: \${error}\`);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
mongoose.connection.on("disconnected", () => {
|
|
487
|
-
logger.warn("MongoDB disconnected");
|
|
488
|
-
});
|
|
489
|
-
};
|
|
490
|
-
`;
|
|
491
|
-
};
|
|
492
|
-
const generateBackendSeed = () => {
|
|
493
|
-
return `import {
|
|
494
|
-
APIError,
|
|
495
|
-
createBetterAuth,
|
|
496
|
-
getMongoClientFromMongoose,
|
|
497
|
-
logger,
|
|
498
|
-
runSeedCli,
|
|
499
|
-
seedBetterAuthUser,
|
|
500
|
-
type SeedStep,
|
|
501
|
-
type UserModel as TerrenoAuthUserModel,
|
|
502
|
-
} from "@terreno/api";
|
|
503
|
-
import mongoose from "mongoose";
|
|
504
|
-
|
|
505
|
-
import {User} from "../models/user";
|
|
506
|
-
import {buildBetterAuthConfig} from "../utils/betterAuthConfig";
|
|
507
|
-
import {connectToMongoDB} from "../utils/database";
|
|
508
|
-
|
|
509
|
-
const SEED_USERS = [
|
|
510
|
-
{
|
|
511
|
-
admin: false,
|
|
512
|
-
email: "test@example.com",
|
|
513
|
-
name: "Test User",
|
|
514
|
-
password: "testpassword123",
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
admin: true,
|
|
518
|
-
email: "admin@example.com",
|
|
519
|
-
name: "Admin User",
|
|
520
|
-
password: "testpassword123",
|
|
521
|
-
},
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
const seedSteps: SeedStep[] = [
|
|
525
|
-
{
|
|
526
|
-
description: "Create login-ready development users and reconcile profile fields",
|
|
527
|
-
name: "users",
|
|
528
|
-
run: async (context) => {
|
|
529
|
-
if (context.dryRun) {
|
|
530
|
-
for (const user of SEED_USERS) {
|
|
531
|
-
const existing = await User.findByEmail(user.email);
|
|
532
|
-
context.changes.push({
|
|
533
|
-
change: existing ? "updated" : "created",
|
|
534
|
-
count: 1,
|
|
535
|
-
key: JSON.stringify({email: user.email}),
|
|
536
|
-
model: User.modelName,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const config = buildBetterAuthConfig();
|
|
543
|
-
if (!config) {
|
|
544
|
-
throw new APIError({
|
|
545
|
-
status: 500,
|
|
546
|
-
title: "Seed users require Better Auth to be enabled",
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
const auth = createBetterAuth({
|
|
550
|
-
config,
|
|
551
|
-
mongoClient: getMongoClientFromMongoose(),
|
|
552
|
-
userModel: User as unknown as TerrenoAuthUserModel,
|
|
553
|
-
});
|
|
554
|
-
for (const definition of SEED_USERS) {
|
|
555
|
-
const user = await seedBetterAuthUser({
|
|
556
|
-
auth,
|
|
557
|
-
user: definition,
|
|
558
|
-
userModel: User as unknown as TerrenoAuthUserModel,
|
|
559
|
-
});
|
|
560
|
-
const appUser = user as unknown as {
|
|
561
|
-
admin: boolean;
|
|
562
|
-
save: () => Promise<unknown>;
|
|
563
|
-
};
|
|
564
|
-
appUser.admin = definition.admin;
|
|
565
|
-
await appUser.save();
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
];
|
|
570
|
-
|
|
571
|
-
const main = async (): Promise<void> => {
|
|
572
|
-
const cli = await runSeedCli({
|
|
573
|
-
allowProductionReset: () => process.env.ALLOW_SEED_RESET === "true",
|
|
574
|
-
connect: connectToMongoDB,
|
|
575
|
-
disconnect: async () => {
|
|
576
|
-
await mongoose.disconnect();
|
|
577
|
-
},
|
|
578
|
-
name: "bun run seed",
|
|
579
|
-
steps: seedSteps,
|
|
580
|
-
});
|
|
581
|
-
if (cli.help) {
|
|
582
|
-
logger.info(cli.help);
|
|
583
|
-
}
|
|
584
|
-
process.exit(cli.exitCode);
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
main().catch((error: unknown) => {
|
|
588
|
-
logger.error(\`Seed failed: \${error}\`);
|
|
589
|
-
process.exit(1);
|
|
590
|
-
});
|
|
591
|
-
`;
|
|
592
|
-
};
|
|
593
|
-
const generateBackendBetterAuthConfig = (args) => {
|
|
594
|
-
const { appName } = args;
|
|
595
|
-
return `import type {AuthProvider, BetterAuthConfig} from "@terreno/api";
|
|
596
|
-
|
|
597
|
-
const DEFAULT_BETTER_AUTH_SECRET = "${appName}-better-auth-secret-dev-only-32chars";
|
|
598
|
-
const DEFAULT_BETTER_AUTH_URL = "http://localhost:4000";
|
|
599
|
-
const DEFAULT_WEB_ORIGINS = ["http://localhost:8082", "http://127.0.0.1:8082"];
|
|
600
|
-
const APP_SCHEMES = ["${appName}://", "exp://"];
|
|
601
|
-
|
|
602
|
-
export const getAuthProvider = (): AuthProvider => {
|
|
603
|
-
const provider = process.env.AUTH_PROVIDER as AuthProvider | undefined;
|
|
604
|
-
return provider ?? "better-auth";
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
export const getWebOrigins = (): string[] => {
|
|
608
|
-
const origins = new Set<string>(DEFAULT_WEB_ORIGINS);
|
|
609
|
-
const fromEnv = process.env.CORS_ORIGINS?.split(",")
|
|
610
|
-
.map((value) => value.trim())
|
|
611
|
-
.filter(Boolean);
|
|
612
|
-
for (const origin of fromEnv ?? []) {
|
|
613
|
-
origins.add(origin);
|
|
614
|
-
}
|
|
615
|
-
return [...origins];
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
export const buildBetterAuthConfig = (): BetterAuthConfig | undefined => {
|
|
619
|
-
if (getAuthProvider() !== "better-auth") {
|
|
620
|
-
return undefined;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
baseURL: process.env.BETTER_AUTH_URL ?? DEFAULT_BETTER_AUTH_URL,
|
|
625
|
-
crossDomainCookies: process.env.CROSS_DOMAIN_AUTH_COOKIES === "true",
|
|
626
|
-
enabled: true,
|
|
627
|
-
secret: process.env.BETTER_AUTH_SECRET ?? DEFAULT_BETTER_AUTH_SECRET,
|
|
628
|
-
trustedOrigins: [...APP_SCHEMES, ...getWebOrigins()],
|
|
629
|
-
};
|
|
630
|
-
};
|
|
631
|
-
`;
|
|
632
|
-
};
|
|
633
|
-
const generateBackendModelPlugins = () => {
|
|
634
|
-
return `import {
|
|
635
|
-
createdUpdatedPlugin,
|
|
636
|
-
findExactlyOne,
|
|
637
|
-
findOneOrNone,
|
|
638
|
-
isDeletedPlugin,
|
|
639
|
-
} from "@terreno/api";
|
|
640
|
-
import type mongoose from "mongoose";
|
|
641
|
-
|
|
642
|
-
// noExplicitAny: Leaving open for flexibility
|
|
643
|
-
// biome-ignore lint/suspicious/noExplicitAny: Leaving open for flexibility
|
|
644
|
-
export function addDefaultPlugins(schema: mongoose.Schema<any, any, any, any>): void {
|
|
645
|
-
schema.plugin(createdUpdatedPlugin);
|
|
646
|
-
schema.plugin(isDeletedPlugin);
|
|
647
|
-
schema.plugin(findOneOrNone);
|
|
648
|
-
schema.plugin(findExactlyOne);
|
|
649
|
-
}
|
|
650
|
-
`;
|
|
651
|
-
};
|
|
652
|
-
const generateBackendUserModel = () => {
|
|
653
|
-
return `import mongoose from "mongoose";
|
|
654
|
-
import passportLocalMongoose from "passport-local-mongoose";
|
|
655
|
-
import type {UserDocument, UserModel} from "../types";
|
|
656
|
-
import {addDefaultPlugins} from "./modelPlugins";
|
|
657
|
-
|
|
658
|
-
const userSchema = new mongoose.Schema<UserDocument, UserModel>(
|
|
659
|
-
{
|
|
660
|
-
admin: {
|
|
661
|
-
default: false,
|
|
662
|
-
type: Boolean,
|
|
663
|
-
},
|
|
664
|
-
betterAuthId: {
|
|
665
|
-
index: true,
|
|
666
|
-
sparse: true,
|
|
667
|
-
type: String,
|
|
668
|
-
},
|
|
669
|
-
email: {
|
|
670
|
-
lowercase: true,
|
|
671
|
-
required: true,
|
|
672
|
-
trim: true,
|
|
673
|
-
type: String,
|
|
674
|
-
unique: true,
|
|
675
|
-
},
|
|
676
|
-
name: {
|
|
677
|
-
required: true,
|
|
678
|
-
trim: true,
|
|
679
|
-
type: String,
|
|
680
|
-
},
|
|
681
|
-
},
|
|
682
|
-
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
userSchema.plugin(passportLocalMongoose, {
|
|
686
|
-
usernameField: "email",
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
addDefaultPlugins(userSchema);
|
|
690
|
-
|
|
691
|
-
userSchema.method("getDisplayName", function (this: UserDocument): string {
|
|
692
|
-
return this.name;
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
export const User = mongoose.model<UserDocument, UserModel>("User", userSchema);
|
|
696
|
-
|
|
697
|
-
User.findByEmail = async function (email: string): Promise<UserDocument | null> {
|
|
698
|
-
return this.findOneOrNone({email: email.toLowerCase()});
|
|
699
|
-
};
|
|
700
|
-
`;
|
|
701
|
-
};
|
|
702
|
-
const generateBackendAppConfiguration = (args) => {
|
|
703
|
-
const { appDisplayName } = args;
|
|
704
|
-
return `import {configurationPlugin, createdUpdatedPlugin} from "@terreno/api";
|
|
705
|
-
import mongoose, {Schema} from "mongoose";
|
|
706
|
-
|
|
707
|
-
const generalSchema = new Schema(
|
|
708
|
-
{
|
|
709
|
-
appName: {
|
|
710
|
-
default: "${appDisplayName}",
|
|
711
|
-
description: "Display name of the application",
|
|
712
|
-
type: String,
|
|
713
|
-
},
|
|
714
|
-
maintenanceMode: {
|
|
715
|
-
default: false,
|
|
716
|
-
description: "When enabled, the app returns 503 for all non-admin requests",
|
|
717
|
-
type: Boolean,
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
{_id: false}
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
const debugSchema = new Schema(
|
|
724
|
-
{
|
|
725
|
-
websocketsDebug: {
|
|
726
|
-
default: false,
|
|
727
|
-
description: "Enable verbose WebSocket and realtime sync debug logging",
|
|
728
|
-
type: Boolean,
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
{_id: false}
|
|
732
|
-
);
|
|
733
|
-
|
|
734
|
-
export interface AppConfigDocument {
|
|
735
|
-
general: {
|
|
736
|
-
appName: string;
|
|
737
|
-
maintenanceMode: boolean;
|
|
738
|
-
};
|
|
739
|
-
debug: {
|
|
740
|
-
websocketsDebug: boolean;
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const appConfigSchema = new Schema<AppConfigDocument>(
|
|
745
|
-
{
|
|
746
|
-
general: {
|
|
747
|
-
description: "General application settings",
|
|
748
|
-
type: generalSchema,
|
|
749
|
-
},
|
|
750
|
-
debug: {
|
|
751
|
-
description: "Debug and diagnostic settings",
|
|
752
|
-
type: debugSchema,
|
|
753
|
-
},
|
|
754
|
-
},
|
|
755
|
-
{strict: "throw", toJSON: {virtuals: true}, toObject: {virtuals: true}}
|
|
756
|
-
);
|
|
757
|
-
|
|
758
|
-
appConfigSchema.plugin(configurationPlugin);
|
|
759
|
-
appConfigSchema.plugin(createdUpdatedPlugin);
|
|
760
|
-
|
|
761
|
-
export const AppConfiguration = mongoose.model<AppConfigDocument>(
|
|
762
|
-
"AppConfiguration",
|
|
763
|
-
appConfigSchema
|
|
764
|
-
);
|
|
765
|
-
`;
|
|
766
|
-
};
|
|
767
|
-
const generateBackendModelsIndex = () => {
|
|
768
|
-
return `export * from "./appConfiguration";
|
|
769
|
-
export * from "./user";
|
|
770
|
-
`;
|
|
771
|
-
};
|
|
772
|
-
const generateBackendUserRoutes = () => {
|
|
773
|
-
return `import {Permissions, modelRouter} from "@terreno/api";
|
|
774
|
-
import {User} from "../models/user";
|
|
775
|
-
|
|
776
|
-
export const userRouter = modelRouter("/users", User, {
|
|
777
|
-
permissions: {
|
|
778
|
-
create: [Permissions.IsAdmin],
|
|
779
|
-
delete: [Permissions.IsAdmin],
|
|
780
|
-
list: [Permissions.IsAuthenticated],
|
|
781
|
-
read: [Permissions.IsAuthenticated],
|
|
782
|
-
update: [Permissions.IsAdmin],
|
|
783
|
-
},
|
|
784
|
-
queryFields: ["email", "name"],
|
|
785
|
-
sort: "name",
|
|
786
|
-
});
|
|
787
|
-
`;
|
|
788
|
-
};
|
|
789
|
-
const generateBackendTypes = () => {
|
|
790
|
-
return `export * from "./models";
|
|
791
|
-
`;
|
|
792
|
-
};
|
|
793
|
-
const generateBackendTypesModels = () => {
|
|
794
|
-
return `export * from "./userTypes";
|
|
795
|
-
`;
|
|
796
|
-
};
|
|
797
|
-
const generateBackendUserTypes = () => {
|
|
798
|
-
return `import type {APIErrorConstructor} from "@terreno/api";
|
|
799
|
-
import type mongoose from "mongoose";
|
|
800
|
-
import type {Document, Model} from "mongoose";
|
|
801
|
-
|
|
802
|
-
type ModelQuery<T> = Partial<Record<keyof T, unknown>> & Record<string, unknown>;
|
|
803
|
-
|
|
804
|
-
export interface DefaultStatics<T> {
|
|
805
|
-
findOneOrNone(
|
|
806
|
-
query: ModelQuery<T>,
|
|
807
|
-
errorArgs?: Partial<APIErrorConstructor>
|
|
808
|
-
): Promise<(Document & T) | null>;
|
|
809
|
-
|
|
810
|
-
findExactlyOne(
|
|
811
|
-
query: ModelQuery<T>,
|
|
812
|
-
errorArgs?: Partial<APIErrorConstructor>
|
|
813
|
-
): Promise<Document & T>;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
export interface DefaultPluginFields {
|
|
817
|
-
created: Date;
|
|
818
|
-
updated: Date;
|
|
819
|
-
deleted: boolean;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
export type DefaultModel<T> = Model<T & DefaultPluginFields> & DefaultStatics<T>;
|
|
823
|
-
export type DefaultDoc = mongoose.Document<mongoose.Types.ObjectId> & DefaultPluginFields;
|
|
824
|
-
|
|
825
|
-
export type UserMethods = {
|
|
826
|
-
getDisplayName: (this: UserDocument) => string;
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
export type UserStatics = DefaultStatics<UserDocument> & {
|
|
830
|
-
findByEmail: (this: UserModel, email: string) => Promise<UserDocument | null>;
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
export type UserModel = DefaultModel<UserDocument> &
|
|
834
|
-
UserStatics &
|
|
835
|
-
mongoose.PassportLocalModel<UserDocument>;
|
|
836
|
-
|
|
837
|
-
export type UserSchema = mongoose.Schema<UserDocument, UserModel, UserMethods>;
|
|
838
|
-
|
|
839
|
-
export type UserDocument = DefaultDoc &
|
|
840
|
-
UserMethods &
|
|
841
|
-
mongoose.PassportLocalDocument & {
|
|
842
|
-
admin: boolean;
|
|
843
|
-
betterAuthId?: string;
|
|
844
|
-
email: string;
|
|
845
|
-
name: string;
|
|
846
|
-
};
|
|
847
|
-
`;
|
|
848
|
-
};
|
|
849
|
-
// Frontend file generators
|
|
850
|
-
const generateFrontendPackageJson = (args) => {
|
|
851
|
-
const { appName } = args;
|
|
852
|
-
return JSON.stringify({
|
|
853
|
-
dependencies: {
|
|
854
|
-
"@expo/vector-icons": "^15.1.1",
|
|
855
|
-
"@react-native-async-storage/async-storage": "2.2.0",
|
|
856
|
-
"@reduxjs/toolkit": "^2.11.1",
|
|
857
|
-
"@sentry/react": "^10.29.0",
|
|
858
|
-
"@terreno/admin-frontend": "latest",
|
|
859
|
-
"@terreno/rtk": "latest",
|
|
860
|
-
"@terreno/syncdb": "latest",
|
|
861
|
-
"@terreno/ui": "latest",
|
|
862
|
-
expo: "~57.0.14",
|
|
863
|
-
"expo-constants": "~18.0.13",
|
|
864
|
-
"expo-router": "~57.0.14",
|
|
865
|
-
"expo-sqlite": "~16.0.10",
|
|
866
|
-
lodash: "^4.18.1",
|
|
867
|
-
luxon: "^3.7.2",
|
|
868
|
-
react: "19.2.3",
|
|
869
|
-
"react-dom": "19.2.3",
|
|
870
|
-
"react-native": "0.86.2",
|
|
871
|
-
"react-native-reanimated": "4.5.1",
|
|
872
|
-
"react-redux": "^9.2.0",
|
|
873
|
-
"redux-persist": "^6.0.0",
|
|
874
|
-
},
|
|
875
|
-
devDependencies: {
|
|
876
|
-
"@biomejs/biome": "^2.3.6",
|
|
877
|
-
"@playwright/test": "^1.58.2",
|
|
878
|
-
"@rtk-query/codegen-openapi": "^2.2.0",
|
|
879
|
-
"@types/lodash": "^4.17.17",
|
|
880
|
-
"@types/luxon": "^3.7.1",
|
|
881
|
-
"@types/react": "~19.2.14",
|
|
882
|
-
tsx: "^4.23.12",
|
|
883
|
-
typescript: "~6.0.3",
|
|
884
|
-
},
|
|
885
|
-
main: "expo-router/entry",
|
|
886
|
-
name: `@${appName}/frontend`,
|
|
887
|
-
private: true,
|
|
888
|
-
scripts: {
|
|
889
|
-
android: "EXPO_UNSTABLE_MCP_SERVER=1 bun expo start --android --port 8082",
|
|
890
|
-
compile: "tsc",
|
|
891
|
-
ios: "EXPO_UNSTABLE_MCP_SERVER=1 bun expo start --ios --port 8082",
|
|
892
|
-
lint: "bun biome check .",
|
|
893
|
-
"lint:fix": "bun biome check --write .",
|
|
894
|
-
"lint:unsafefix": "biome check --write . --unsafe",
|
|
895
|
-
sdk: "bun scripts/generate-sdk.ts && bun biome check --write scripts/generate-sdk.ts",
|
|
896
|
-
start: "EXPO_UNSTABLE_MCP_SERVER=1 bun expo start --port 8082",
|
|
897
|
-
test: "bun test",
|
|
898
|
-
web: "EXPO_UNSTABLE_MCP_SERVER=1 bun expo start --web --port 8082",
|
|
899
|
-
},
|
|
900
|
-
version: "1.0.0",
|
|
901
|
-
}, null, 2);
|
|
902
|
-
};
|
|
903
|
-
const generateFrontendAppJson = (args) => {
|
|
904
|
-
const { appDisplayName, appName } = args;
|
|
905
|
-
return JSON.stringify({
|
|
906
|
-
// No icon/splash/favicon keys: Expo falls back to its built-in defaults, so a freshly
|
|
907
|
-
// generated app bundles without any binary assets. Add your own branding later and
|
|
908
|
-
// point `icon`, `android.adaptiveIcon`, `splash`, and `web.favicon` at those files.
|
|
909
|
-
expo: {
|
|
910
|
-
experiments: {
|
|
911
|
-
typedRoutes: true,
|
|
912
|
-
},
|
|
913
|
-
ios: {
|
|
914
|
-
supportsTablet: true,
|
|
915
|
-
},
|
|
916
|
-
name: appDisplayName,
|
|
917
|
-
orientation: "portrait",
|
|
918
|
-
plugins: ["expo-router"],
|
|
919
|
-
scheme: appName,
|
|
920
|
-
slug: appName,
|
|
921
|
-
userInterfaceStyle: "automatic",
|
|
922
|
-
version: "1.0.0",
|
|
923
|
-
web: {
|
|
924
|
-
bundler: "metro",
|
|
925
|
-
output: "static",
|
|
926
|
-
},
|
|
927
|
-
},
|
|
928
|
-
}, null, 2);
|
|
929
|
-
};
|
|
930
|
-
const generateFrontendTsConfig = () => {
|
|
931
|
-
// No `baseUrl`: it is deprecated as of TypeScript 6 and makes `tsc` abort with TS5101
|
|
932
|
-
// before checking a single file. `paths` entries are relative to this tsconfig instead.
|
|
933
|
-
return JSON.stringify({
|
|
934
|
-
compilerOptions: {
|
|
935
|
-
allowJs: true,
|
|
936
|
-
allowSyntheticDefaultImports: true,
|
|
937
|
-
esModuleInterop: true,
|
|
938
|
-
jsx: "react-jsx",
|
|
939
|
-
lib: ["DOM", "ESNext"],
|
|
940
|
-
module: "esnext",
|
|
941
|
-
moduleResolution: "bundler",
|
|
942
|
-
noEmit: true,
|
|
943
|
-
paths: {
|
|
944
|
-
"@/*": ["./*"],
|
|
945
|
-
"@components/*": ["./components/*"],
|
|
946
|
-
"@store": ["./store/index"],
|
|
947
|
-
"@store/*": ["./store/*"],
|
|
948
|
-
"@utils": ["./utils/index"],
|
|
949
|
-
"@utils/*": ["./utils/*"],
|
|
950
|
-
},
|
|
951
|
-
resolveJsonModule: true,
|
|
952
|
-
skipLibCheck: true,
|
|
953
|
-
strict: true,
|
|
954
|
-
target: "ESNext",
|
|
955
|
-
types: ["react-native", "@types/react"],
|
|
956
|
-
},
|
|
957
|
-
extends: "expo/tsconfig.base",
|
|
958
|
-
include: ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
|
|
959
|
-
}, null, 2);
|
|
960
|
-
};
|
|
961
|
-
const generateFrontendMetroConfig = () => {
|
|
962
|
-
return `const {getDefaultConfig} = require("expo/metro-config");
|
|
963
|
-
const path = require("path");
|
|
964
|
-
|
|
965
|
-
const config = getDefaultConfig(__dirname);
|
|
966
|
-
|
|
967
|
-
const defaultResolveRequest = config.resolver.resolveRequest;
|
|
968
|
-
|
|
969
|
-
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
970
|
-
// @terreno/admin-frontend imports jspdf for its consent-PDF export. jspdf's CommonJS and
|
|
971
|
-
// Node builds wrap an AMD-style \`require(["html2canvas"], cb)\` call that Metro's static
|
|
972
|
-
// transform cannot parse, which fails the whole bundle — including Expo Router's static
|
|
973
|
-
// web render, which resolves under a "node" condition and would otherwise pick
|
|
974
|
-
// jspdf.node.min.js. Pin every jspdf request to the ESM browser build on web, and drop it
|
|
975
|
-
// on native, where PDF export is unavailable and jspdf's browser/Node APIs are missing.
|
|
976
|
-
if (moduleName === "jspdf" || moduleName.startsWith("jspdf/dist/jspdf.node")) {
|
|
977
|
-
if (platform !== "web") {
|
|
978
|
-
return {type: "empty"};
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
const jspdfDir = path.dirname(require.resolve("jspdf/package.json", {paths: [__dirname]}));
|
|
982
|
-
return {
|
|
983
|
-
filePath: path.join(jspdfDir, "dist", "jspdf.es.min.js"),
|
|
984
|
-
type: "sourceFile",
|
|
985
|
-
};
|
|
986
|
-
} catch {
|
|
987
|
-
// jspdf is not installed — let Metro report the failure itself.
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
return defaultResolveRequest
|
|
992
|
-
? defaultResolveRequest(context, moduleName, platform)
|
|
993
|
-
: context.resolveRequest(context, moduleName, platform);
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
module.exports = config;
|
|
997
|
-
`;
|
|
998
|
-
};
|
|
999
|
-
const generateFrontendTsConfigCodegen = () => {
|
|
1000
|
-
return JSON.stringify({
|
|
1001
|
-
compilerOptions: {
|
|
1002
|
-
esModuleInterop: true,
|
|
1003
|
-
module: "commonjs",
|
|
1004
|
-
moduleResolution: "node",
|
|
1005
|
-
resolveJsonModule: true,
|
|
1006
|
-
skipLibCheck: true,
|
|
1007
|
-
target: "ES2020",
|
|
1008
|
-
},
|
|
1009
|
-
}, null, 2);
|
|
1010
|
-
};
|
|
1011
|
-
const generateFrontendBiomeJsonc = () => {
|
|
1012
|
-
return `{
|
|
1013
|
-
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
1014
|
-
"assist": {
|
|
1015
|
-
"actions": {
|
|
1016
|
-
"source": {
|
|
1017
|
-
"organizeImports": "on",
|
|
1018
|
-
"useSortedAttributes": "on",
|
|
1019
|
-
"useSortedKeys": "on"
|
|
1020
|
-
}
|
|
1021
|
-
},
|
|
1022
|
-
"enabled": true
|
|
1023
|
-
},
|
|
1024
|
-
"files": {
|
|
1025
|
-
"includes": [
|
|
1026
|
-
"package.json",
|
|
1027
|
-
"app/**/*.ts",
|
|
1028
|
-
"app/**/*.tsx",
|
|
1029
|
-
"components/**/*.ts",
|
|
1030
|
-
"components/**/*.tsx",
|
|
1031
|
-
"store/**/*.ts",
|
|
1032
|
-
"store/**/*.tsx",
|
|
1033
|
-
"utils/**/*.ts",
|
|
1034
|
-
"utils/**/*.tsx",
|
|
1035
|
-
"constants/**/*.ts",
|
|
1036
|
-
"scripts/**/*.ts",
|
|
1037
|
-
"!!**/node_modules",
|
|
1038
|
-
"!!**/dist",
|
|
1039
|
-
"!!**/build",
|
|
1040
|
-
"!!**/coverage",
|
|
1041
|
-
"!!**/.expo",
|
|
1042
|
-
"!!**/.next",
|
|
1043
|
-
"!!**/generated",
|
|
1044
|
-
"!!**/.git"
|
|
1045
|
-
]
|
|
1046
|
-
},
|
|
1047
|
-
"formatter": {
|
|
1048
|
-
"enabled": true,
|
|
1049
|
-
"indentStyle": "space",
|
|
1050
|
-
"indentWidth": 2,
|
|
1051
|
-
"lineWidth": 100
|
|
1052
|
-
},
|
|
1053
|
-
"javascript": {
|
|
1054
|
-
"formatter": {
|
|
1055
|
-
"arrowParentheses": "always",
|
|
1056
|
-
"bracketSpacing": false,
|
|
1057
|
-
"jsxQuoteStyle": "double",
|
|
1058
|
-
"quoteStyle": "double",
|
|
1059
|
-
"semicolons": "always",
|
|
1060
|
-
"trailingCommas": "es5"
|
|
1061
|
-
},
|
|
1062
|
-
// TODO: Remove once we don't need to import React from 'react' in our JSX files.
|
|
1063
|
-
"jsxRuntime": "reactClassic"
|
|
1064
|
-
},
|
|
1065
|
-
"linter": {
|
|
1066
|
-
"enabled": true,
|
|
1067
|
-
"rules": {
|
|
1068
|
-
"nursery": {
|
|
1069
|
-
"noFloatingPromises": "error",
|
|
1070
|
-
"noMisusedPromises": "error",
|
|
1071
|
-
"useExhaustiveSwitchCases": "error"
|
|
1072
|
-
},
|
|
1073
|
-
"recommended": true,
|
|
1074
|
-
"suspicious": {
|
|
1075
|
-
"noConsole": {
|
|
1076
|
-
"level": "error",
|
|
1077
|
-
"options": {
|
|
1078
|
-
"allow": ["assert", "debug", "error", "info", "warn"]
|
|
1079
|
-
}
|
|
1080
|
-
},
|
|
1081
|
-
"noExplicitAny": "off",
|
|
1082
|
-
"noImplicitAnyLet": "off"
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
},
|
|
1086
|
-
"vcs": {
|
|
1087
|
-
"clientKind": "git",
|
|
1088
|
-
"defaultBranch": "master",
|
|
1089
|
-
"enabled": true
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
`;
|
|
1093
|
-
};
|
|
1094
|
-
const generateFrontendOpenApiConfig = () => {
|
|
1095
|
-
return `import type {ConfigFile} from "@rtk-query/codegen-openapi";
|
|
1096
|
-
|
|
1097
|
-
const config: ConfigFile = {
|
|
1098
|
-
apiFile: "@terreno/rtk",
|
|
1099
|
-
apiImport: "emptySplitApi",
|
|
1100
|
-
argSuffix: "Args",
|
|
1101
|
-
exportName: "openapi",
|
|
1102
|
-
flattenArg: true,
|
|
1103
|
-
hooks: true,
|
|
1104
|
-
outputFile: "./store/openApiSdk.ts",
|
|
1105
|
-
responseSuffix: "Res",
|
|
1106
|
-
schemaFile: process.env.OPENAPI_URL ?? "http://localhost:4000/openapi.json",
|
|
1107
|
-
tag: true,
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
|
-
export default config;
|
|
1111
|
-
`;
|
|
1112
|
-
};
|
|
1113
|
-
const generateFrontendGenerateSdk = () => {
|
|
1114
|
-
return `#!/usr/bin/env bun
|
|
1115
|
-
|
|
1116
|
-
import {exec} from "node:child_process";
|
|
1117
|
-
import {existsSync, readFileSync, writeFileSync} from "node:fs";
|
|
1118
|
-
import {join} from "node:path";
|
|
1119
|
-
|
|
1120
|
-
const cliPath = join(
|
|
1121
|
-
__dirname,
|
|
1122
|
-
"..",
|
|
1123
|
-
"node_modules",
|
|
1124
|
-
"@rtk-query",
|
|
1125
|
-
"codegen-openapi",
|
|
1126
|
-
"lib",
|
|
1127
|
-
"bin",
|
|
1128
|
-
"cli.mjs"
|
|
1129
|
-
);
|
|
1130
|
-
const configPath = join(__dirname, "..", "openapi-config.ts");
|
|
1131
|
-
const tsConfigPath = join(__dirname, "..", "tsconfig.codegen.json");
|
|
1132
|
-
|
|
1133
|
-
const command = \`TS_NODE_PROJECT=\${tsConfigPath} tsx \${cliPath} \${configPath}\`;
|
|
1134
|
-
|
|
1135
|
-
exec(command, (error, _stdout, stderr) => {
|
|
1136
|
-
if (error) {
|
|
1137
|
-
console.error(\`Error: \${error.message}\`);
|
|
1138
|
-
process.exit(1);
|
|
1139
|
-
}
|
|
1140
|
-
if (stderr) {
|
|
1141
|
-
console.error(\`stderr: \${stderr}\`);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const sdkPath = join(__dirname, "..", "store", "openApiSdk.ts");
|
|
1145
|
-
|
|
1146
|
-
if (existsSync(sdkPath)) {
|
|
1147
|
-
let content = readFileSync(sdkPath, "utf8");
|
|
1148
|
-
content = content.replace(/^export const \\{\\} = injectedRtkApi;\\n?/m, "");
|
|
1149
|
-
writeFileSync(sdkPath, content, "utf8");
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
exec(
|
|
1153
|
-
"bunx biome check --unsafe --write store/openApiSdk.ts",
|
|
1154
|
-
{cwd: join(__dirname, "..")},
|
|
1155
|
-
(formatError) => {
|
|
1156
|
-
if (formatError) {
|
|
1157
|
-
console.error(\`Formatting error: \${formatError.message}\`);
|
|
1158
|
-
process.exit(1);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
);
|
|
1162
|
-
});
|
|
1163
|
-
`;
|
|
1164
|
-
};
|
|
1165
|
-
const generateFrontendRootLayout = (_args) => {
|
|
1166
|
-
return `import {DefaultTheme, ThemeProvider} from "expo-router/react-navigation";
|
|
1167
|
-
import {Stack} from "expo-router";
|
|
1168
|
-
import {useEffect} from "react";
|
|
1169
|
-
import "react-native-reanimated";
|
|
1170
|
-
import {baseUrl, selectBetterAuthIsLoading, selectBetterAuthUserId} from "@terreno/rtk";
|
|
1171
|
-
import {SyncDbProvider} from "@terreno/syncdb/react";
|
|
1172
|
-
import {Spinner, TerrenoProvider} from "@terreno/ui";
|
|
1173
|
-
import {Provider, useSelector} from "react-redux";
|
|
1174
|
-
import {PersistGate} from "redux-persist/integration/react";
|
|
1175
|
-
import store, {persistor, syncBetterAuthSession} from "@/store/index";
|
|
1176
|
-
import {setSyncDbReady, syncDb} from "@/store/syncdb";
|
|
1177
|
-
|
|
1178
|
-
export {ErrorBoundary} from "expo-router";
|
|
1179
|
-
|
|
1180
|
-
export const unstable_settings = {
|
|
1181
|
-
initialRouteName: "(tabs)",
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
// TerrenoProvider loads the Nunito/Titillium Web families @terreno/ui renders with, so this
|
|
1185
|
-
// app ships no font assets of its own. Add \`useFonts\` here only for extra custom faces.
|
|
1186
|
-
export default function RootLayout(): React.ReactElement | null {
|
|
1187
|
-
return (
|
|
1188
|
-
<Provider store={store}>
|
|
1189
|
-
<PersistGate loading={null} persistor={persistor}>
|
|
1190
|
-
<TerrenoProvider openAPISpecUrl={\`\${baseUrl}/openapi.json\`}>
|
|
1191
|
-
<RootLayoutNav />
|
|
1192
|
-
</TerrenoProvider>
|
|
1193
|
-
</PersistGate>
|
|
1194
|
-
</Provider>
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
function RootLayoutNav(): React.ReactElement {
|
|
1199
|
-
const userId = useSelector(selectBetterAuthUserId) ?? undefined;
|
|
1200
|
-
const isAuthLoading = useSelector(selectBetterAuthIsLoading);
|
|
1201
|
-
|
|
1202
|
-
// Hydrate Better Auth session into Redux on startup.
|
|
1203
|
-
useEffect(() => {
|
|
1204
|
-
void syncBetterAuthSession(store.dispatch);
|
|
1205
|
-
}, []);
|
|
1206
|
-
|
|
1207
|
-
// Start syncdb after login; stop on logout.
|
|
1208
|
-
useEffect(() => {
|
|
1209
|
-
if (!userId) {
|
|
1210
|
-
setSyncDbReady(false);
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
let stopped = false;
|
|
1214
|
-
void syncDb
|
|
1215
|
-
.start()
|
|
1216
|
-
.then(() => {
|
|
1217
|
-
if (!stopped) {
|
|
1218
|
-
setSyncDbReady(true);
|
|
1219
|
-
}
|
|
1220
|
-
})
|
|
1221
|
-
.catch((startError: unknown) => {
|
|
1222
|
-
console.error("[syncdb] start failed", startError);
|
|
1223
|
-
setSyncDbReady(false);
|
|
1224
|
-
});
|
|
1225
|
-
return () => {
|
|
1226
|
-
stopped = true;
|
|
1227
|
-
setSyncDbReady(false);
|
|
1228
|
-
void syncDb.stop();
|
|
1229
|
-
};
|
|
1230
|
-
}, [userId]);
|
|
1231
|
-
|
|
1232
|
-
if (isAuthLoading) {
|
|
1233
|
-
return <Spinner />;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
return (
|
|
1237
|
-
<SyncDbProvider client={syncDb}>
|
|
1238
|
-
<ThemeProvider value={DefaultTheme}>
|
|
1239
|
-
{/* Stack.Protected is the supported way to gate routes on auth state. Rendering
|
|
1240
|
-
Stack.Screen children behind a raw conditional (or a fragment) crashes the
|
|
1241
|
-
navigator, because Stack reads its screen config off the child elements. */}
|
|
1242
|
-
<Stack>
|
|
1243
|
-
<Stack.Protected guard={!userId}>
|
|
1244
|
-
<Stack.Screen name="login" options={{headerShown: false}} />
|
|
1245
|
-
<Stack.Screen name="signup" options={{headerShown: false}} />
|
|
1246
|
-
</Stack.Protected>
|
|
1247
|
-
<Stack.Protected guard={Boolean(userId)}>
|
|
1248
|
-
<Stack.Screen name="(tabs)" options={{headerShown: false}} />
|
|
1249
|
-
</Stack.Protected>
|
|
1250
|
-
</Stack>
|
|
1251
|
-
</ThemeProvider>
|
|
1252
|
-
</SyncDbProvider>
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
`;
|
|
1256
|
-
};
|
|
1257
|
-
const generateFrontendLogin = () => {
|
|
1258
|
-
return `import {baseUrl} from "@terreno/rtk";
|
|
1259
|
-
import {LoginScreen} from "@terreno/ui";
|
|
1260
|
-
import {useRouter} from "expo-router";
|
|
1261
|
-
import type React from "react";
|
|
1262
|
-
import {useCallback, useState} from "react";
|
|
1263
|
-
import {betterAuthClient} from "@/lib/betterAuth";
|
|
1264
|
-
import {syncBetterAuthSession, useAppDispatch} from "@/store/index";
|
|
1265
|
-
|
|
1266
|
-
const Login: React.FC = () => {
|
|
1267
|
-
const router = useRouter();
|
|
1268
|
-
const dispatch = useAppDispatch();
|
|
1269
|
-
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
|
1270
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1271
|
-
|
|
1272
|
-
const handleSubmit = useCallback(
|
|
1273
|
-
async (values: Record<string, string>): Promise<void> => {
|
|
1274
|
-
const {email, password} = values;
|
|
1275
|
-
setErrorMessage(undefined);
|
|
1276
|
-
setIsSubmitting(true);
|
|
1277
|
-
try {
|
|
1278
|
-
const result = await betterAuthClient.signIn.email({email, password});
|
|
1279
|
-
if (result.error) {
|
|
1280
|
-
setErrorMessage(result.error.message ?? "Sign in failed.");
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
await syncBetterAuthSession(dispatch);
|
|
1284
|
-
router.replace("/(tabs)");
|
|
1285
|
-
} catch (error: unknown) {
|
|
1286
|
-
console.error("[login] Sign in threw", {baseUrl, error});
|
|
1287
|
-
setErrorMessage("Sign in failed. Please try again.");
|
|
1288
|
-
} finally {
|
|
1289
|
-
setIsSubmitting(false);
|
|
1290
|
-
}
|
|
1291
|
-
},
|
|
1292
|
-
[dispatch, router]
|
|
1293
|
-
);
|
|
1294
|
-
|
|
1295
|
-
return (
|
|
1296
|
-
<LoginScreen
|
|
1297
|
-
error={errorMessage}
|
|
1298
|
-
fields={[
|
|
1299
|
-
{autoComplete: "off", label: "Email", name: "email", required: true, type: "email"},
|
|
1300
|
-
{label: "Password", name: "password", required: true, type: "password"},
|
|
1301
|
-
]}
|
|
1302
|
-
loading={isSubmitting}
|
|
1303
|
-
onSignUpPress={() => router.push("/signup")}
|
|
1304
|
-
onSubmit={handleSubmit}
|
|
1305
|
-
title="Welcome"
|
|
1306
|
-
/>
|
|
1307
|
-
);
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
export default Login;
|
|
1311
|
-
`;
|
|
1312
|
-
};
|
|
1313
|
-
const generateFrontendSignup = () => {
|
|
1314
|
-
return `import {SignUpScreen, simplePasswordRequirements} from "@terreno/ui";
|
|
1315
|
-
import {useRouter} from "expo-router";
|
|
1316
|
-
import type React from "react";
|
|
1317
|
-
import {useCallback, useState} from "react";
|
|
1318
|
-
import {betterAuthClient} from "@/lib/betterAuth";
|
|
1319
|
-
import {syncBetterAuthSession, useAppDispatch} from "@/store/index";
|
|
1320
|
-
|
|
1321
|
-
const SignUp: React.FC = () => {
|
|
1322
|
-
const router = useRouter();
|
|
1323
|
-
const dispatch = useAppDispatch();
|
|
1324
|
-
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
|
1325
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
1326
|
-
|
|
1327
|
-
const handleSubmit = useCallback(
|
|
1328
|
-
async (values: Record<string, string>): Promise<void> => {
|
|
1329
|
-
const {email, password, name} = values;
|
|
1330
|
-
setErrorMessage(undefined);
|
|
1331
|
-
setIsSubmitting(true);
|
|
1332
|
-
try {
|
|
1333
|
-
const result = await betterAuthClient.signUp.email({email, name, password});
|
|
1334
|
-
if (result.error) {
|
|
1335
|
-
setErrorMessage(result.error.message ?? "Sign up failed.");
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
await syncBetterAuthSession(dispatch);
|
|
1339
|
-
router.replace("/(tabs)");
|
|
1340
|
-
} catch {
|
|
1341
|
-
setErrorMessage("Sign up failed. Please try again.");
|
|
1342
|
-
} finally {
|
|
1343
|
-
setIsSubmitting(false);
|
|
1344
|
-
}
|
|
1345
|
-
},
|
|
1346
|
-
[dispatch, router]
|
|
1347
|
-
);
|
|
1348
|
-
|
|
1349
|
-
return (
|
|
1350
|
-
<SignUpScreen
|
|
1351
|
-
error={errorMessage}
|
|
1352
|
-
fields={[
|
|
1353
|
-
{label: "Name", name: "name", required: true, type: "text"},
|
|
1354
|
-
{autoComplete: "off", label: "Email", name: "email", required: true, type: "email"},
|
|
1355
|
-
{label: "Password", name: "password", required: true, type: "password"},
|
|
1356
|
-
]}
|
|
1357
|
-
loading={isSubmitting}
|
|
1358
|
-
onLoginPress={() => router.back()}
|
|
1359
|
-
onSubmit={handleSubmit}
|
|
1360
|
-
passwordRequirements={simplePasswordRequirements}
|
|
1361
|
-
/>
|
|
1362
|
-
);
|
|
1363
|
-
};
|
|
1364
|
-
|
|
1365
|
-
export default SignUp;
|
|
1366
|
-
`;
|
|
1367
|
-
};
|
|
1368
|
-
const generateFrontendTabsLayout = () => {
|
|
1369
|
-
return `import FontAwesome from "@expo/vector-icons/FontAwesome";
|
|
1370
|
-
import {Tabs} from "expo-router";
|
|
1371
|
-
import type React from "react";
|
|
1372
|
-
import {colors} from "@/constants/theme";
|
|
1373
|
-
|
|
1374
|
-
const TabBarIcon: React.FC<{
|
|
1375
|
-
name: React.ComponentProps<typeof FontAwesome>["name"];
|
|
1376
|
-
color: string;
|
|
1377
|
-
}> = ({name, color}) => {
|
|
1378
|
-
return <FontAwesome color={color} name={name} size={24} style={{marginBottom: -3}} />;
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
|
-
const TabLayout: React.FC = () => {
|
|
1382
|
-
return (
|
|
1383
|
-
<Tabs
|
|
1384
|
-
screenOptions={{
|
|
1385
|
-
tabBarActiveTintColor: colors.tint,
|
|
1386
|
-
}}
|
|
1387
|
-
>
|
|
1388
|
-
<Tabs.Screen
|
|
1389
|
-
name="index"
|
|
1390
|
-
options={{
|
|
1391
|
-
tabBarIcon: ({color}) => <TabBarIcon color={color} name="home" />,
|
|
1392
|
-
title: "Home",
|
|
1393
|
-
}}
|
|
1394
|
-
/>
|
|
1395
|
-
<Tabs.Screen
|
|
1396
|
-
name="profile"
|
|
1397
|
-
options={{
|
|
1398
|
-
headerShown: false,
|
|
1399
|
-
tabBarIcon: ({color}) => <TabBarIcon color={color} name="user" />,
|
|
1400
|
-
title: "Profile",
|
|
1401
|
-
}}
|
|
1402
|
-
/>
|
|
1403
|
-
<Tabs.Screen
|
|
1404
|
-
name="admin"
|
|
1405
|
-
options={{
|
|
1406
|
-
headerShown: false,
|
|
1407
|
-
tabBarIcon: ({color}) => <TabBarIcon color={color} name="cog" />,
|
|
1408
|
-
title: "Admin",
|
|
1409
|
-
}}
|
|
1410
|
-
/>
|
|
1411
|
-
</Tabs>
|
|
1412
|
-
);
|
|
1413
|
-
};
|
|
1414
|
-
|
|
1415
|
-
export default TabLayout;
|
|
1416
|
-
`;
|
|
1417
|
-
};
|
|
1418
|
-
const generateFrontendTabsIndex = (args) => {
|
|
1419
|
-
const { appDisplayName } = args;
|
|
1420
|
-
return `import {Box, Heading, Page, Text} from "@terreno/ui";
|
|
1421
|
-
import type React from "react";
|
|
1422
|
-
|
|
1423
|
-
const HomeScreen: React.FC = () => {
|
|
1424
|
-
return (
|
|
1425
|
-
<Page navigation={undefined} title="Home">
|
|
1426
|
-
<Box padding={4} gap={4}>
|
|
1427
|
-
<Heading>Welcome to ${appDisplayName}</Heading>
|
|
1428
|
-
<Text>Your app is ready for development!</Text>
|
|
1429
|
-
<Text color="secondary">
|
|
1430
|
-
Start by adding models to the backend and screens to the frontend.
|
|
1431
|
-
</Text>
|
|
1432
|
-
</Box>
|
|
1433
|
-
</Page>
|
|
1434
|
-
);
|
|
1435
|
-
};
|
|
1436
|
-
|
|
1437
|
-
export default HomeScreen;
|
|
1438
|
-
`;
|
|
1439
|
-
};
|
|
1440
|
-
const generateFrontendTabsProfile = () => {
|
|
1441
|
-
return `import {Box, Button, Card, Heading, Page, Spinner, TapToEdit, Text} from "@terreno/ui";
|
|
1442
|
-
import type React from "react";
|
|
1443
|
-
import {useCallback, useEffect, useState} from "react";
|
|
1444
|
-
import {signOut} from "@/lib/betterAuth";
|
|
1445
|
-
import {logout, syncBetterAuthSession, useAppDispatch} from "@/store/index";
|
|
1446
|
-
import {useGetMeQuery, usePatchMeMutation} from "@/store/sdk";
|
|
1447
|
-
|
|
1448
|
-
const ProfileScreen: React.FC = () => {
|
|
1449
|
-
const dispatch = useAppDispatch();
|
|
1450
|
-
const {data: profile, isLoading} = useGetMeQuery();
|
|
1451
|
-
const [updateProfile] = usePatchMeMutation();
|
|
1452
|
-
const [name, setName] = useState<string>("");
|
|
1453
|
-
const [email, setEmail] = useState<string>("");
|
|
1454
|
-
const [password, setPassword] = useState<string>("");
|
|
1455
|
-
const [saveError, setSaveError] = useState<string | null>(null);
|
|
1456
|
-
|
|
1457
|
-
const user = profile?.data;
|
|
1458
|
-
|
|
1459
|
-
// Copy the server name into local state without resetting an in-progress email edit.
|
|
1460
|
-
useEffect(() => {
|
|
1461
|
-
if (!user) {
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
setName(user.name || "");
|
|
1465
|
-
}, [user?.name]);
|
|
1466
|
-
|
|
1467
|
-
// Copy the server email into local state without resetting an in-progress name edit.
|
|
1468
|
-
useEffect(() => {
|
|
1469
|
-
if (!user) {
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
setEmail(user.email || "");
|
|
1473
|
-
}, [user?.email]);
|
|
1474
|
-
|
|
1475
|
-
const handleLogout = useCallback(async (): Promise<void> => {
|
|
1476
|
-
await signOut();
|
|
1477
|
-
dispatch(logout());
|
|
1478
|
-
await syncBetterAuthSession(dispatch);
|
|
1479
|
-
}, [dispatch]);
|
|
1480
|
-
|
|
1481
|
-
const handleSaveName = useCallback(
|
|
1482
|
-
async (value: string): Promise<void> => {
|
|
1483
|
-
setSaveError(null);
|
|
1484
|
-
try {
|
|
1485
|
-
await updateProfile({name: value}).unwrap();
|
|
1486
|
-
} catch (err) {
|
|
1487
|
-
console.error("Error updating name:", err);
|
|
1488
|
-
setSaveError("Failed to update name");
|
|
1489
|
-
}
|
|
1490
|
-
},
|
|
1491
|
-
[updateProfile]
|
|
1492
|
-
);
|
|
1493
|
-
|
|
1494
|
-
const handleSaveEmail = useCallback(
|
|
1495
|
-
async (value: string): Promise<void> => {
|
|
1496
|
-
setSaveError(null);
|
|
1497
|
-
try {
|
|
1498
|
-
await updateProfile({email: value}).unwrap();
|
|
1499
|
-
} catch (err) {
|
|
1500
|
-
console.error("Error updating email:", err);
|
|
1501
|
-
setSaveError("Failed to update email");
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
[updateProfile]
|
|
1505
|
-
);
|
|
1506
|
-
|
|
1507
|
-
const handleSavePassword = useCallback(
|
|
1508
|
-
async (value: string): Promise<void> => {
|
|
1509
|
-
if (!value) {
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
setSaveError(null);
|
|
1513
|
-
try {
|
|
1514
|
-
await updateProfile({password: value}).unwrap();
|
|
1515
|
-
setPassword("");
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
console.error("Error updating password:", err);
|
|
1518
|
-
setSaveError("Failed to update password");
|
|
1519
|
-
}
|
|
1520
|
-
},
|
|
1521
|
-
[updateProfile]
|
|
1522
|
-
);
|
|
1523
|
-
|
|
1524
|
-
if (isLoading) {
|
|
1525
|
-
return (
|
|
1526
|
-
<Page navigation={undefined} title="Profile">
|
|
1527
|
-
<Box alignItems="center" flex="grow" justifyContent="center" padding={4}>
|
|
1528
|
-
<Spinner />
|
|
1529
|
-
</Box>
|
|
1530
|
-
</Page>
|
|
1531
|
-
);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
return (
|
|
1535
|
-
<Page navigation={undefined} scroll title="Profile">
|
|
1536
|
-
<Box gap={4} padding={4}>
|
|
1537
|
-
<Heading>Profile</Heading>
|
|
1538
|
-
<Card>
|
|
1539
|
-
<Box gap={4}>
|
|
1540
|
-
<TapToEdit
|
|
1541
|
-
onSave={handleSaveName}
|
|
1542
|
-
setValue={setName}
|
|
1543
|
-
title="Name"
|
|
1544
|
-
type="text"
|
|
1545
|
-
value={name}
|
|
1546
|
-
/>
|
|
1547
|
-
<TapToEdit
|
|
1548
|
-
onSave={handleSaveEmail}
|
|
1549
|
-
setValue={setEmail}
|
|
1550
|
-
title="Email"
|
|
1551
|
-
type="email"
|
|
1552
|
-
value={email}
|
|
1553
|
-
/>
|
|
1554
|
-
<TapToEdit
|
|
1555
|
-
helperText="Leave blank to keep your current password"
|
|
1556
|
-
onSave={handleSavePassword}
|
|
1557
|
-
setValue={setPassword}
|
|
1558
|
-
title="New Password"
|
|
1559
|
-
type="password"
|
|
1560
|
-
value={password}
|
|
1561
|
-
/>
|
|
1562
|
-
{saveError && <Text color="error">{saveError}</Text>}
|
|
1563
|
-
</Box>
|
|
1564
|
-
</Card>
|
|
1565
|
-
<Box marginTop={4}>
|
|
1566
|
-
<Button fullWidth onClick={handleLogout} text="Logout" variant="outline" />
|
|
1567
|
-
</Box>
|
|
1568
|
-
</Box>
|
|
1569
|
-
</Page>
|
|
1570
|
-
);
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
|
-
export default ProfileScreen;
|
|
1574
|
-
`;
|
|
1575
|
-
};
|
|
1576
|
-
const generateFrontendAdminLayout = () => {
|
|
1577
|
-
return `import {Stack} from "expo-router";
|
|
1578
|
-
import type React from "react";
|
|
1579
|
-
|
|
1580
|
-
const AdminLayout: React.FC = () => {
|
|
1581
|
-
return <Stack screenOptions={{headerShown: false}} />;
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
export default AdminLayout;
|
|
1585
|
-
`;
|
|
1586
|
-
};
|
|
1587
|
-
const generateFrontendAdminIndex = () => {
|
|
1588
|
-
return `import {AdminModelList} from "@terreno/admin-frontend";
|
|
1589
|
-
import type React from "react";
|
|
1590
|
-
import {terrenoApi} from "@/store/sdk";
|
|
1591
|
-
|
|
1592
|
-
const AdminListScreen: React.FC = () => {
|
|
1593
|
-
return (
|
|
1594
|
-
<AdminModelList
|
|
1595
|
-
api={terrenoApi}
|
|
1596
|
-
baseUrl="/admin"
|
|
1597
|
-
configurationPath="/admin/configuration"
|
|
1598
|
-
/>
|
|
1599
|
-
);
|
|
1600
|
-
};
|
|
1601
|
-
|
|
1602
|
-
export default AdminListScreen;
|
|
1603
|
-
`;
|
|
1604
|
-
};
|
|
1605
|
-
const generateFrontendAdminConfiguration = () => {
|
|
1606
|
-
return `import {ConfigurationScreen} from "@terreno/admin-frontend";
|
|
1607
|
-
import type React from "react";
|
|
1608
|
-
import {terrenoApi} from "@/store/sdk";
|
|
1609
|
-
|
|
1610
|
-
const ConfigurationPage: React.FC = () => {
|
|
1611
|
-
return <ConfigurationScreen api={terrenoApi} />;
|
|
1612
|
-
};
|
|
1613
|
-
|
|
1614
|
-
export default ConfigurationPage;
|
|
1615
|
-
`;
|
|
1616
|
-
};
|
|
1617
|
-
const generateFrontendNotFound = () => {
|
|
1618
|
-
return `import {Box, Text} from "@terreno/ui";
|
|
1619
|
-
import {Link, Stack} from "expo-router";
|
|
1620
|
-
import type React from "react";
|
|
1621
|
-
|
|
1622
|
-
const NotFoundScreen: React.FC = () => {
|
|
1623
|
-
return (
|
|
1624
|
-
<>
|
|
1625
|
-
<Stack.Screen options={{title: "Oops!"}} />
|
|
1626
|
-
<Box flex={1} alignItems="center" justifyContent="center" padding={4}>
|
|
1627
|
-
<Text size="lg" weight="bold">
|
|
1628
|
-
This screen doesn't exist.
|
|
1629
|
-
</Text>
|
|
1630
|
-
<Link href="/" style={{marginTop: 16}}>
|
|
1631
|
-
<Text color="link">Go to home screen</Text>
|
|
1632
|
-
</Link>
|
|
1633
|
-
</Box>
|
|
1634
|
-
</>
|
|
1635
|
-
);
|
|
1636
|
-
};
|
|
1637
|
-
|
|
1638
|
-
export default NotFoundScreen;
|
|
1639
|
-
`;
|
|
1640
|
-
};
|
|
1641
|
-
const generateFrontendStoreIndex = () => {
|
|
1642
|
-
return `import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
1643
|
-
import {combineReducers, configureStore} from "@reduxjs/toolkit";
|
|
1644
|
-
import {generateBetterAuthSlice, registerTerrenoDevStore} from "@terreno/rtk";
|
|
1645
|
-
import {DateTime} from "luxon";
|
|
1646
|
-
import {useDispatch} from "react-redux";
|
|
1647
|
-
import type {Storage} from "redux-persist";
|
|
1648
|
-
import {persistReducer, persistStore} from "redux-persist";
|
|
1649
|
-
import {betterAuthClient} from "@/lib/betterAuth";
|
|
1650
|
-
|
|
1651
|
-
import appState from "./appState";
|
|
1652
|
-
import {rtkQueryErrorMiddleware} from "./errors";
|
|
1653
|
-
import {terrenoApi} from "./sdk";
|
|
1654
|
-
|
|
1655
|
-
const betterAuth = generateBetterAuthSlice({authClient: betterAuthClient});
|
|
1656
|
-
|
|
1657
|
-
export const logout = betterAuth.actions.logout;
|
|
1658
|
-
export const syncBetterAuthSession = betterAuth.syncSession;
|
|
1659
|
-
|
|
1660
|
-
const createSafeStorage = (): Storage => {
|
|
1661
|
-
return {
|
|
1662
|
-
getItem: async (key: string): Promise<string | null> => {
|
|
1663
|
-
if (typeof window !== "undefined") {
|
|
1664
|
-
return AsyncStorage.getItem(key);
|
|
1665
|
-
}
|
|
1666
|
-
return null;
|
|
1667
|
-
},
|
|
1668
|
-
removeItem: async (key: string): Promise<void> => {
|
|
1669
|
-
if (typeof window !== "undefined") {
|
|
1670
|
-
return AsyncStorage.removeItem(key);
|
|
1671
|
-
}
|
|
1672
|
-
},
|
|
1673
|
-
setItem: async (key: string, value: string): Promise<void> => {
|
|
1674
|
-
if (typeof window !== "undefined") {
|
|
1675
|
-
return AsyncStorage.setItem(key, value);
|
|
1676
|
-
}
|
|
1677
|
-
},
|
|
1678
|
-
};
|
|
1679
|
-
};
|
|
1680
|
-
|
|
1681
|
-
const persistConfig = {
|
|
1682
|
-
blacklist: ["terreno-rtk", "betterAuth"],
|
|
1683
|
-
key: "root",
|
|
1684
|
-
storage: createSafeStorage(),
|
|
1685
|
-
timeout: 0,
|
|
1686
|
-
version: 1,
|
|
1687
|
-
};
|
|
1688
|
-
|
|
1689
|
-
const rootReducer = combineReducers({
|
|
1690
|
-
appState,
|
|
1691
|
-
betterAuth: betterAuth.reducer,
|
|
1692
|
-
"terreno-rtk": terrenoApi.reducer,
|
|
1693
|
-
});
|
|
1694
|
-
|
|
1695
|
-
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
|
1696
|
-
|
|
1697
|
-
const store = configureStore({
|
|
1698
|
-
devTools: process.env.NODE_ENV !== "production" && {
|
|
1699
|
-
name: \`App-\${
|
|
1700
|
-
typeof window !== "undefined"
|
|
1701
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: Window name assignment
|
|
1702
|
-
? window.name || ((window.name = \`Window-\${DateTime.now().toFormat("HH:mm:ss")}\`))
|
|
1703
|
-
: "Unknown"
|
|
1704
|
-
}\`,
|
|
1705
|
-
},
|
|
1706
|
-
middleware: (getDefaultMiddleware) => {
|
|
1707
|
-
return getDefaultMiddleware({
|
|
1708
|
-
immutableCheck: false,
|
|
1709
|
-
serializableCheck: false,
|
|
1710
|
-
thunk: true,
|
|
1711
|
-
}).concat([
|
|
1712
|
-
...betterAuth.middleware,
|
|
1713
|
-
// noExplicitAny: RTK Query middleware typing
|
|
1714
|
-
// biome-ignore lint/suspicious/noExplicitAny: RTK Query middleware typing
|
|
1715
|
-
terrenoApi.middleware as any,
|
|
1716
|
-
rtkQueryErrorMiddleware,
|
|
1717
|
-
// noExplicitAny: Middleware array typing
|
|
1718
|
-
// biome-ignore lint/suspicious/noExplicitAny: Middleware array typing
|
|
1719
|
-
]) as any;
|
|
1720
|
-
},
|
|
1721
|
-
reducer: persistedReducer,
|
|
1722
|
-
});
|
|
1723
|
-
|
|
1724
|
-
registerTerrenoDevStore(store);
|
|
1725
|
-
|
|
1726
|
-
export const persistor = persistStore(store);
|
|
1727
|
-
|
|
1728
|
-
export type RootState = ReturnType<typeof store.getState>;
|
|
1729
|
-
export type AppDispatch = typeof store.dispatch;
|
|
1730
|
-
|
|
1731
|
-
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
1732
|
-
|
|
1733
|
-
export default store;
|
|
1734
|
-
`;
|
|
1735
|
-
};
|
|
1736
|
-
const generateFrontendStoreAppState = () => {
|
|
1737
|
-
return `import {createSlice, type PayloadAction} from "@reduxjs/toolkit";
|
|
1738
|
-
import type {RootState} from "@terreno/rtk";
|
|
1739
|
-
import {type TypedUseSelectorHook, useSelector} from "react-redux";
|
|
1740
|
-
|
|
1741
|
-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
1742
|
-
|
|
1743
|
-
export type AppState = {
|
|
1744
|
-
darkMode: boolean;
|
|
1745
|
-
language: string;
|
|
1746
|
-
};
|
|
1747
|
-
|
|
1748
|
-
const initialState: AppState = {
|
|
1749
|
-
darkMode: false,
|
|
1750
|
-
language: "en",
|
|
1751
|
-
};
|
|
1752
|
-
|
|
1753
|
-
export const appStateSlice = createSlice({
|
|
1754
|
-
initialState,
|
|
1755
|
-
name: "appState",
|
|
1756
|
-
reducers: {
|
|
1757
|
-
resetAppState: () => initialState,
|
|
1758
|
-
setDarkMode: (state, action: PayloadAction<boolean>) => {
|
|
1759
|
-
state.darkMode = action.payload;
|
|
1760
|
-
},
|
|
1761
|
-
setLanguage: (state, action: PayloadAction<string>) => {
|
|
1762
|
-
state.language = action.payload;
|
|
1763
|
-
},
|
|
1764
|
-
},
|
|
1765
|
-
});
|
|
1766
|
-
|
|
1767
|
-
export const {setDarkMode, setLanguage, resetAppState} = appStateSlice.actions;
|
|
1768
|
-
|
|
1769
|
-
export const useSelectDarkMode = (): boolean => {
|
|
1770
|
-
return useAppSelector((state: RootState): boolean => {
|
|
1771
|
-
return state.appState.darkMode;
|
|
1772
|
-
});
|
|
1773
|
-
};
|
|
1774
|
-
|
|
1775
|
-
export const useSelectLanguage = (): string => {
|
|
1776
|
-
return useAppSelector((state: RootState): string => {
|
|
1777
|
-
return state.appState.language;
|
|
1778
|
-
});
|
|
1779
|
-
};
|
|
1780
|
-
|
|
1781
|
-
export default appStateSlice.reducer;
|
|
1782
|
-
`;
|
|
1783
|
-
};
|
|
1784
|
-
const generateFrontendStoreErrors = () => {
|
|
1785
|
-
return `import type {Middleware} from "@reduxjs/toolkit";
|
|
1786
|
-
import {useToast} from "@terreno/ui";
|
|
1787
|
-
|
|
1788
|
-
const ignoredErrors = [
|
|
1789
|
-
"Password or username is incorrect",
|
|
1790
|
-
"Token refresh failed with 401",
|
|
1791
|
-
"Failed to refresh token",
|
|
1792
|
-
];
|
|
1793
|
-
|
|
1794
|
-
// noExplicitAny: Generic middleware
|
|
1795
|
-
// biome-ignore lint/suspicious/noExplicitAny: Generic middleware
|
|
1796
|
-
export const rtkQueryErrorMiddleware: Middleware = () => (next) => (action: any) => {
|
|
1797
|
-
if (action?.error && action?.payload) {
|
|
1798
|
-
const errorMessage =
|
|
1799
|
-
action.payload?.data?.title ??
|
|
1800
|
-
action.payload?.data?.message ??
|
|
1801
|
-
action.payload?.error ??
|
|
1802
|
-
JSON.stringify(action.payload);
|
|
1803
|
-
|
|
1804
|
-
let endpointInfo = "unknown endpoint";
|
|
1805
|
-
if (action.meta?.baseQueryMeta?.request?.method && action.meta?.baseQueryMeta?.request?.url) {
|
|
1806
|
-
endpointInfo = \`\${action.meta.baseQueryMeta.request.url} \${action.meta.baseQueryMeta.request.method}\`;
|
|
1807
|
-
} else if (action.meta?.arg?.endpointName) {
|
|
1808
|
-
endpointInfo = \`\${action.meta.arg.endpointName} rejected \${action.meta.arg.type || ""}\`;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
const message = \`\${endpointInfo.trim()}: \${errorMessage}\`;
|
|
1812
|
-
console.debug(message);
|
|
1813
|
-
|
|
1814
|
-
if (action.payload.status === 404 || action.payload.status === 401) {
|
|
1815
|
-
return next(action);
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
const shouldIgnore = ignoredErrors.some((err) => errorMessage.includes(err));
|
|
1819
|
-
if (!shouldIgnore) {
|
|
1820
|
-
console.warn(message);
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
return next(action);
|
|
1825
|
-
};
|
|
1826
|
-
|
|
1827
|
-
export const useSentryAndToast = (): ((errorMessage: string) => void) => {
|
|
1828
|
-
const toast = useToast();
|
|
1829
|
-
return (error: string): void => {
|
|
1830
|
-
if (!error) {
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
toast.error(error);
|
|
1834
|
-
console.warn(\`Error: \${error}\`);
|
|
1835
|
-
};
|
|
1836
|
-
};
|
|
1837
|
-
`;
|
|
1838
|
-
};
|
|
1839
|
-
const generateFrontendStoreSdk = () => {
|
|
1840
|
-
return `import {generateTags} from "@terreno/rtk";
|
|
1841
|
-
import startCase from "lodash/startCase";
|
|
1842
|
-
|
|
1843
|
-
import {addTagTypes, openapi} from "./openApiSdk";
|
|
1844
|
-
|
|
1845
|
-
export interface ProfileResponse {
|
|
1846
|
-
data: {
|
|
1847
|
-
_id: string;
|
|
1848
|
-
id: string;
|
|
1849
|
-
email: string;
|
|
1850
|
-
name: string;
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
export interface UpdateProfileRequest {
|
|
1855
|
-
name?: string;
|
|
1856
|
-
email?: string;
|
|
1857
|
-
password?: string;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
export const terrenoApi = openapi
|
|
1861
|
-
.injectEndpoints({
|
|
1862
|
-
endpoints: (builder) => ({
|
|
1863
|
-
getMe: builder.query<ProfileResponse, void>({
|
|
1864
|
-
providesTags: ["profile"],
|
|
1865
|
-
query: () => ({
|
|
1866
|
-
method: "GET",
|
|
1867
|
-
url: "/auth/me",
|
|
1868
|
-
}),
|
|
1869
|
-
}),
|
|
1870
|
-
patchMe: builder.mutation<ProfileResponse, UpdateProfileRequest>({
|
|
1871
|
-
invalidatesTags: ["profile"],
|
|
1872
|
-
query: (body) => ({
|
|
1873
|
-
body,
|
|
1874
|
-
method: "PATCH",
|
|
1875
|
-
url: "/auth/me",
|
|
1876
|
-
}),
|
|
1877
|
-
}),
|
|
1878
|
-
}),
|
|
1879
|
-
})
|
|
1880
|
-
.enhanceEndpoints({
|
|
1881
|
-
addTagTypes: ["profile"],
|
|
1882
|
-
endpoints: {
|
|
1883
|
-
...generateTags(openapi, [...addTagTypes]),
|
|
1884
|
-
},
|
|
1885
|
-
});
|
|
1886
|
-
|
|
1887
|
-
export const {
|
|
1888
|
-
useGetMeQuery,
|
|
1889
|
-
usePatchMeMutation,
|
|
1890
|
-
} = terrenoApi;
|
|
1891
|
-
export * from "./openApiSdk";
|
|
1892
|
-
|
|
1893
|
-
type OpenApiEndpoints = Record<string, unknown>;
|
|
1894
|
-
|
|
1895
|
-
export const getSdkHook = (
|
|
1896
|
-
modelName: string,
|
|
1897
|
-
type: "list" | "read" | "create" | "update" | "remove"
|
|
1898
|
-
): Record<string, unknown> => {
|
|
1899
|
-
const modelPath = startCase(modelName).replace(/\\s/g, "");
|
|
1900
|
-
const endpoints = openapi.endpoints as OpenApiEndpoints;
|
|
1901
|
-
switch (type) {
|
|
1902
|
-
case "list":
|
|
1903
|
-
return endpoints[\`get\${modelPath}\`] as Record<string, unknown>;
|
|
1904
|
-
case "read":
|
|
1905
|
-
return endpoints[\`get\${modelPath}ById\`] as Record<string, unknown>;
|
|
1906
|
-
case "create":
|
|
1907
|
-
return endpoints[\`post\${modelPath}\`] as Record<string, unknown>;
|
|
1908
|
-
case "update":
|
|
1909
|
-
return endpoints[\`patch\${modelPath}ById\`] as Record<string, unknown>;
|
|
1910
|
-
case "remove":
|
|
1911
|
-
return endpoints[\`delete\${modelPath}ById\`] as Record<string, unknown>;
|
|
1912
|
-
default:
|
|
1913
|
-
throw new Error(\`Invalid SDK hook: \${modelName}/\${type}\`);
|
|
1914
|
-
}
|
|
1915
|
-
};
|
|
1916
|
-
`;
|
|
1917
|
-
};
|
|
1918
|
-
const generateFrontendStoreOpenApiSdk = () => {
|
|
1919
|
-
return `// THIS FILE IS AUTO-GENERATED. DO NOT EDIT.
|
|
1920
|
-
// Run "bun run sdk" to regenerate this file from the backend OpenAPI spec.
|
|
1921
|
-
|
|
1922
|
-
import {emptySplitApi as api} from "@terreno/rtk";
|
|
1923
|
-
export const addTagTypes = ["Users", "Auth"] as const;
|
|
1924
|
-
const injectedRtkApi = api
|
|
1925
|
-
.enhanceEndpoints({
|
|
1926
|
-
addTagTypes,
|
|
1927
|
-
})
|
|
1928
|
-
.injectEndpoints({
|
|
1929
|
-
endpoints: () => ({}),
|
|
1930
|
-
overrideExisting: false,
|
|
1931
|
-
});
|
|
1932
|
-
export {injectedRtkApi as openapi};
|
|
1933
|
-
`;
|
|
1934
|
-
};
|
|
1935
|
-
const generateFrontendTheme = () => {
|
|
1936
|
-
return `// Theme configuration for the app
|
|
1937
|
-
// Override these values to customize the app's appearance
|
|
1938
|
-
// Colors are based on @terreno/ui's default theme primitives
|
|
1939
|
-
|
|
1940
|
-
export const primitives = {
|
|
1941
|
-
// Primary colors (teal/cyan)
|
|
1942
|
-
primary000: "#EBFAFF",
|
|
1943
|
-
primary050: "#BCE9F7",
|
|
1944
|
-
primary100: "#90D8F0",
|
|
1945
|
-
primary200: "#73CAE8",
|
|
1946
|
-
primary300: "#40B8E0",
|
|
1947
|
-
primary400: "#0E9DCD",
|
|
1948
|
-
primary500: "#0086B3",
|
|
1949
|
-
primary600: "#0A7092",
|
|
1950
|
-
primary700: "#035D7E",
|
|
1951
|
-
primary800: "#004B64",
|
|
1952
|
-
primary900: "#013749",
|
|
1953
|
-
|
|
1954
|
-
// Secondary colors (dark teal)
|
|
1955
|
-
secondary000: "#F2F9FA",
|
|
1956
|
-
secondary050: "#D7E5EA",
|
|
1957
|
-
secondary100: "#B6CDD5",
|
|
1958
|
-
secondary200: "#9EB7BF",
|
|
1959
|
-
secondary300: "#87A1AA",
|
|
1960
|
-
secondary400: "#608997",
|
|
1961
|
-
secondary500: "#2B6072",
|
|
1962
|
-
secondary600: "#1C4E5F",
|
|
1963
|
-
secondary700: "#0F3D4D",
|
|
1964
|
-
secondary800: "#092E3A",
|
|
1965
|
-
secondary900: "#041E27",
|
|
1966
|
-
|
|
1967
|
-
// Accent colors (gold/yellow)
|
|
1968
|
-
accent000: "#FFFDF7",
|
|
1969
|
-
accent050: "#FCECC2",
|
|
1970
|
-
accent100: "#F9E0A1",
|
|
1971
|
-
accent200: "#F7D582",
|
|
1972
|
-
accent300: "#F2CB62",
|
|
1973
|
-
accent400: "#E5B132",
|
|
1974
|
-
accent500: "#D69C0E",
|
|
1975
|
-
accent600: "#B58201",
|
|
1976
|
-
accent700: "#956A00",
|
|
1977
|
-
accent800: "#543C00",
|
|
1978
|
-
accent900: "#332400",
|
|
1979
|
-
|
|
1980
|
-
// Neutral colors (grays)
|
|
1981
|
-
neutral000: "#FFFFFF",
|
|
1982
|
-
neutral050: "#F2F2F2",
|
|
1983
|
-
neutral100: "#E6E6E6",
|
|
1984
|
-
neutral200: "#D9D9D9",
|
|
1985
|
-
neutral300: "#CDCDCD",
|
|
1986
|
-
neutral400: "#B3B3B3",
|
|
1987
|
-
neutral500: "#9A9A9A",
|
|
1988
|
-
neutral600: "#686868",
|
|
1989
|
-
neutral700: "#4E4E4E",
|
|
1990
|
-
neutral800: "#353535",
|
|
1991
|
-
neutral900: "#1C1C1C",
|
|
1992
|
-
|
|
1993
|
-
// Status colors
|
|
1994
|
-
error000: "#FDD7D7",
|
|
1995
|
-
error100: "#D33232",
|
|
1996
|
-
error200: "#BD1111",
|
|
1997
|
-
success000: "#DCF2E2",
|
|
1998
|
-
success100: "#3EA45C",
|
|
1999
|
-
success200: "#1A7F36",
|
|
2000
|
-
warning000: "#FFE3C6",
|
|
2001
|
-
warning100: "#F36719",
|
|
2002
|
-
warning200: "#B14202",
|
|
2003
|
-
};
|
|
2004
|
-
|
|
2005
|
-
// Semantic color mappings - override these to change app appearance
|
|
2006
|
-
export const colors = {
|
|
2007
|
-
// Backgrounds
|
|
2008
|
-
background: primitives.neutral000,
|
|
2009
|
-
backgroundSecondary: primitives.neutral050,
|
|
2010
|
-
|
|
2011
|
-
// Text
|
|
2012
|
-
text: primitives.neutral900,
|
|
2013
|
-
textSecondary: primitives.neutral600,
|
|
2014
|
-
textInverted: primitives.neutral000,
|
|
2015
|
-
|
|
2016
|
-
// Primary action colors
|
|
2017
|
-
primary: primitives.primary400,
|
|
2018
|
-
primaryDark: primitives.primary600,
|
|
2019
|
-
primaryLight: primitives.primary100,
|
|
2020
|
-
|
|
2021
|
-
// Secondary colors
|
|
2022
|
-
secondary: primitives.secondary500,
|
|
2023
|
-
secondaryDark: primitives.secondary700,
|
|
2024
|
-
secondaryLight: primitives.secondary100,
|
|
2025
|
-
|
|
2026
|
-
// Accent colors
|
|
2027
|
-
accent: primitives.accent500,
|
|
2028
|
-
accentDark: primitives.accent700,
|
|
2029
|
-
accentLight: primitives.accent100,
|
|
2030
|
-
|
|
2031
|
-
// Status colors
|
|
2032
|
-
error: primitives.error100,
|
|
2033
|
-
errorLight: primitives.error000,
|
|
2034
|
-
success: primitives.success100,
|
|
2035
|
-
successLight: primitives.success000,
|
|
2036
|
-
warning: primitives.warning100,
|
|
2037
|
-
warningLight: primitives.warning000,
|
|
2038
|
-
|
|
2039
|
-
// UI elements
|
|
2040
|
-
border: primitives.neutral300,
|
|
2041
|
-
borderFocus: primitives.primary200,
|
|
2042
|
-
icon: primitives.neutral600,
|
|
2043
|
-
tint: primitives.primary400,
|
|
2044
|
-
|
|
2045
|
-
// Tab bar
|
|
2046
|
-
tabIconDefault: primitives.neutral600,
|
|
2047
|
-
tabIconSelected: primitives.primary400,
|
|
2048
|
-
};
|
|
2049
|
-
|
|
2050
|
-
export default colors;
|
|
2051
|
-
`;
|
|
2052
|
-
};
|
|
2053
|
-
const generateFrontendEnv = () => {
|
|
2054
|
-
return `EXPO_PUBLIC_API_URL=http://localhost:4000
|
|
2055
|
-
`;
|
|
2056
|
-
};
|
|
2057
|
-
const generateBackendEnv = (args) => {
|
|
2058
|
-
const { appName } = args;
|
|
2059
|
-
const dbName = appName.replace(/-/g, "_");
|
|
2060
|
-
return `MONGO_URI=mongodb://127.0.0.1:27017/${dbName}?replicaSet=rs0
|
|
2061
|
-
AUTH_PROVIDER=better-auth
|
|
2062
|
-
BETTER_AUTH_SECRET=${appName}-better-auth-secret-dev-only-32chars
|
|
2063
|
-
BETTER_AUTH_URL=http://localhost:4000
|
|
2064
|
-
TOKEN_SECRET=dev-token-secret
|
|
2065
|
-
TOKEN_ISSUER=${appName}-dev
|
|
2066
|
-
REFRESH_TOKEN_SECRET=dev-refresh-secret
|
|
2067
|
-
SESSION_SECRET=dev-session-secret
|
|
2068
|
-
PORT=4000
|
|
2069
|
-
`;
|
|
2070
|
-
};
|
|
2071
|
-
const generateFrontendBetterAuthLib = (args) => {
|
|
2072
|
-
const { appName } = args;
|
|
2073
|
-
return `import {baseUrl, createBetterAuthClient} from "@terreno/rtk";
|
|
2074
|
-
import Constants from "expo-constants";
|
|
2075
|
-
|
|
2076
|
-
const getAppScheme = (): string => {
|
|
2077
|
-
const expoExtra = Constants.expoConfig?.extra;
|
|
2078
|
-
return expoExtra?.scheme ?? "${appName}";
|
|
2079
|
-
};
|
|
2080
|
-
|
|
2081
|
-
export const betterAuthClient = createBetterAuthClient({
|
|
2082
|
-
baseURL: baseUrl,
|
|
2083
|
-
scheme: getAppScheme(),
|
|
2084
|
-
storagePrefix: "${appName}",
|
|
2085
|
-
});
|
|
2086
|
-
|
|
2087
|
-
export const signOut = async (): Promise<void> => {
|
|
2088
|
-
await betterAuthClient.signOut();
|
|
2089
|
-
};
|
|
2090
|
-
`;
|
|
2091
|
-
};
|
|
2092
|
-
const generateFrontendSyncDb = (args) => {
|
|
2093
|
-
const { appName } = args;
|
|
2094
|
-
const dbName = appName.replace(/-/g, "-");
|
|
2095
|
-
return `import {baseUrl} from "@terreno/rtk";
|
|
2096
|
-
import {betterAuthAdapter, createSyncDb, type SyncDb} from "@terreno/syncdb";
|
|
2097
|
-
import {betterAuthClient} from "@/lib/betterAuth";
|
|
2098
|
-
|
|
2099
|
-
export const SYNC_DB_NAME = "${dbName}";
|
|
2100
|
-
|
|
2101
|
-
/** Add synced collection names here as you register modelRouter sync scopes on the backend. */
|
|
2102
|
-
export const SYNC_COLLECTIONS: string[] = [];
|
|
2103
|
-
|
|
2104
|
-
const authProvider = betterAuthAdapter(betterAuthClient, {pollIntervalMs: 60_000});
|
|
2105
|
-
|
|
2106
|
-
export const syncDb: SyncDb = createSyncDb({
|
|
2107
|
-
authProvider,
|
|
2108
|
-
baseUrl,
|
|
2109
|
-
collections: SYNC_COLLECTIONS,
|
|
2110
|
-
name: SYNC_DB_NAME,
|
|
2111
|
-
});
|
|
2112
|
-
|
|
2113
|
-
let syncDbReady = false;
|
|
2114
|
-
const syncDbReadyListeners = new Set<() => void>();
|
|
2115
|
-
|
|
2116
|
-
export const setSyncDbReady = (ready: boolean): void => {
|
|
2117
|
-
if (syncDbReady === ready) {
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
syncDbReady = ready;
|
|
2121
|
-
for (const listener of syncDbReadyListeners) {
|
|
2122
|
-
listener();
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
|
|
2126
|
-
export const subscribeSyncDbReady = (listener: () => void): (() => void) => {
|
|
2127
|
-
syncDbReadyListeners.add(listener);
|
|
2128
|
-
return () => {
|
|
2129
|
-
syncDbReadyListeners.delete(listener);
|
|
2130
|
-
};
|
|
2131
|
-
};
|
|
2132
|
-
|
|
2133
|
-
export const getSyncDbReadySnapshot = (): boolean => syncDbReady;
|
|
2134
|
-
`;
|
|
2135
|
-
};
|
|
2136
|
-
const generateFrontendUtilsIndex = () => {
|
|
2137
|
-
return `export const captureException = (error: unknown): void => {
|
|
2138
|
-
console.error("Captured exception:", error);
|
|
2139
|
-
};
|
|
2140
|
-
|
|
2141
|
-
export const captureMessage = (message: string): void => {
|
|
2142
|
-
console.warn("Captured message:", message);
|
|
2143
|
-
};
|
|
2144
|
-
|
|
2145
|
-
export const createSentryReduxEnhancer = (): unknown => {
|
|
2146
|
-
return undefined;
|
|
2147
|
-
};
|
|
2148
|
-
`;
|
|
2149
|
-
};
|
|
2150
|
-
const generateBackendCiWorkflow = (_args) => {
|
|
2151
|
-
return `name: Backend CI
|
|
2152
|
-
|
|
2153
|
-
on:
|
|
2154
|
-
push:
|
|
2155
|
-
paths:
|
|
2156
|
-
- "backend/**"
|
|
2157
|
-
- ".github/workflows/backend-ci.yml"
|
|
2158
|
-
|
|
2159
|
-
jobs:
|
|
2160
|
-
lint-and-test:
|
|
2161
|
-
name: Backend Lint, Build, and Test
|
|
2162
|
-
runs-on: ubuntu-latest
|
|
2163
|
-
strategy:
|
|
2164
|
-
matrix:
|
|
2165
|
-
mongodb-version: ["6.0"]
|
|
2166
|
-
steps:
|
|
2167
|
-
- uses: actions/checkout@v6
|
|
2168
|
-
|
|
2169
|
-
- name: Setup Bun
|
|
2170
|
-
uses: oven-sh/setup-bun@v2
|
|
2171
|
-
with:
|
|
2172
|
-
bun-version: latest
|
|
2173
|
-
|
|
2174
|
-
- name: Cache Bun dependencies
|
|
2175
|
-
id: cache
|
|
2176
|
-
uses: actions/cache@v5
|
|
2177
|
-
with:
|
|
2178
|
-
path: |
|
|
2179
|
-
~/.bun/install/cache
|
|
2180
|
-
node_modules
|
|
2181
|
-
key: bun-\${{ runner.os }}-\${{ github.ref }}-\${{ hashFiles('bun.lockb', 'package.json') }}
|
|
2182
|
-
restore-keys: |
|
|
2183
|
-
bun-\${{ runner.os }}-\${{ github.ref }}-
|
|
2184
|
-
bun-\${{ runner.os }}-
|
|
2185
|
-
|
|
2186
|
-
- name: Start MongoDB
|
|
2187
|
-
uses: supercharge/mongodb-github-action@1.12.1
|
|
2188
|
-
with:
|
|
2189
|
-
mongodb-version: \${{ matrix.mongodb-version }}
|
|
2190
|
-
|
|
2191
|
-
- name: Install dependencies
|
|
2192
|
-
run: bun install --frozen-lockfile
|
|
2193
|
-
working-directory: backend
|
|
2194
|
-
|
|
2195
|
-
- name: Lint
|
|
2196
|
-
run: bun run lint
|
|
2197
|
-
working-directory: backend
|
|
2198
|
-
|
|
2199
|
-
- name: Build
|
|
2200
|
-
run: bun run compile
|
|
2201
|
-
working-directory: backend
|
|
2202
|
-
|
|
2203
|
-
- name: Test
|
|
2204
|
-
run: bun run test
|
|
2205
|
-
working-directory: backend
|
|
2206
|
-
env:
|
|
2207
|
-
CI: true
|
|
2208
|
-
`;
|
|
2209
|
-
};
|
|
2210
31
|
const generateRulesyncConfig = () => {
|
|
2211
32
|
return JSON.stringify({
|
|
2212
33
|
$schema: "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
|
|
@@ -2533,7 +354,7 @@ const generateAiRulesFiles = (args) => {
|
|
|
2533
354
|
{ content: generateRulesyncConfig(), path: "rulesync.jsonc" },
|
|
2534
355
|
];
|
|
2535
356
|
};
|
|
2536
|
-
|
|
357
|
+
const handleBootstrapAiRulesToolCall = (args) => {
|
|
2537
358
|
const bootstrapArgs = args;
|
|
2538
359
|
if (!bootstrapArgs.appName || !bootstrapArgs.appDisplayName) {
|
|
2539
360
|
return {
|
|
@@ -2604,249 +425,12 @@ ${f.content}
|
|
|
2604
425
|
content: [{ text: instructions + fileContents, type: "text" }],
|
|
2605
426
|
};
|
|
2606
427
|
};
|
|
2607
|
-
const generateFrontendCiWorkflow = (_args) => {
|
|
2608
|
-
return `name: Frontend CI
|
|
2609
|
-
|
|
2610
|
-
on:
|
|
2611
|
-
push:
|
|
2612
|
-
paths:
|
|
2613
|
-
- "frontend/**"
|
|
2614
|
-
- ".github/workflows/frontend-ci.yml"
|
|
2615
|
-
|
|
2616
|
-
jobs:
|
|
2617
|
-
lint-and-test:
|
|
2618
|
-
name: Frontend Lint, Build, and Test
|
|
2619
|
-
runs-on: ubuntu-latest
|
|
2620
|
-
steps:
|
|
2621
|
-
- uses: actions/checkout@v6
|
|
2622
|
-
|
|
2623
|
-
- name: Setup Bun
|
|
2624
|
-
uses: oven-sh/setup-bun@v2
|
|
2625
|
-
with:
|
|
2626
|
-
bun-version: latest
|
|
2627
|
-
|
|
2628
|
-
- name: Cache Bun dependencies
|
|
2629
|
-
id: cache
|
|
2630
|
-
uses: actions/cache@v5
|
|
2631
|
-
with:
|
|
2632
|
-
path: |
|
|
2633
|
-
~/.bun/install/cache
|
|
2634
|
-
node_modules
|
|
2635
|
-
key: bun-\${{ runner.os }}-\${{ github.ref }}-\${{ hashFiles('bun.lockb', 'package.json') }}
|
|
2636
|
-
restore-keys: |
|
|
2637
|
-
bun-\${{ runner.os }}-\${{ github.ref }}-
|
|
2638
|
-
bun-\${{ runner.os }}-
|
|
2639
|
-
|
|
2640
|
-
- name: Install dependencies
|
|
2641
|
-
run: bun install --frozen-lockfile
|
|
2642
|
-
working-directory: frontend
|
|
2643
|
-
|
|
2644
|
-
- name: Lint
|
|
2645
|
-
run: bun run lint
|
|
2646
|
-
working-directory: frontend
|
|
2647
|
-
|
|
2648
|
-
- name: TypeScript compile
|
|
2649
|
-
run: bun run compile
|
|
2650
|
-
working-directory: frontend
|
|
2651
|
-
|
|
2652
|
-
- name: Test
|
|
2653
|
-
run: bun run test
|
|
2654
|
-
working-directory: frontend
|
|
2655
|
-
env:
|
|
2656
|
-
CI: true
|
|
2657
|
-
`;
|
|
2658
|
-
};
|
|
2659
|
-
const getFenceLanguage = (filePath) => {
|
|
2660
|
-
if (filePath.endsWith(".json") || filePath.endsWith(".jsonc")) {
|
|
2661
|
-
return "json";
|
|
2662
|
-
}
|
|
2663
|
-
if (filePath.endsWith(".js")) {
|
|
2664
|
-
return "javascript";
|
|
2665
|
-
}
|
|
2666
|
-
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
|
|
2667
|
-
return "yaml";
|
|
2668
|
-
}
|
|
2669
|
-
if (filePath.endsWith(".md")) {
|
|
2670
|
-
return "markdown";
|
|
2671
|
-
}
|
|
2672
|
-
return "typescript";
|
|
2673
|
-
};
|
|
2674
|
-
const generateAllFiles = (args) => {
|
|
2675
|
-
const frontendDir = `frontend`;
|
|
2676
|
-
const backendDir = `backend`;
|
|
2677
|
-
return [
|
|
2678
|
-
// Root files
|
|
2679
|
-
{ content: generateRootGitignore(), path: ".gitignore" },
|
|
2680
|
-
{ content: generateCursorRules(args), path: ".cursorrules" },
|
|
2681
|
-
{ content: generateMcpSettings(args), path: ".cursor/mcp.json" },
|
|
2682
|
-
{ content: generateClaudeMd(args), path: "CLAUDE.md" },
|
|
2683
|
-
// Backend files
|
|
2684
|
-
{ content: generateBackendPackageJson(args), path: `${backendDir}/package.json` },
|
|
2685
|
-
{ content: generateBackendTsConfig(), path: `${backendDir}/tsconfig.json` },
|
|
2686
|
-
{ content: generateBackendBiomeJsonc(), path: `${backendDir}/biome.jsonc` },
|
|
2687
|
-
{ content: generateBackendIndex(), path: `${backendDir}/src/index.ts` },
|
|
2688
|
-
{ content: generateBackendServer(args), path: `${backendDir}/src/server.ts` },
|
|
2689
|
-
{ content: generateBackendDatabase(args), path: `${backendDir}/src/utils/database.ts` },
|
|
2690
|
-
{ content: generateBackendSeed(), path: `${backendDir}/src/scripts/seed.ts` },
|
|
2691
|
-
{
|
|
2692
|
-
content: generateBackendBetterAuthConfig(args),
|
|
2693
|
-
path: `${backendDir}/src/utils/betterAuthConfig.ts`,
|
|
2694
|
-
},
|
|
2695
|
-
{ content: generateBackendEnv(args), path: `${backendDir}/.env` },
|
|
2696
|
-
{ content: generateBackendModelPlugins(), path: `${backendDir}/src/models/modelPlugins.ts` },
|
|
2697
|
-
{ content: generateBackendUserModel(), path: `${backendDir}/src/models/user.ts` },
|
|
2698
|
-
{
|
|
2699
|
-
content: generateBackendAppConfiguration(args),
|
|
2700
|
-
path: `${backendDir}/src/models/appConfiguration.ts`,
|
|
2701
|
-
},
|
|
2702
|
-
{ content: generateBackendModelsIndex(), path: `${backendDir}/src/models/index.ts` },
|
|
2703
|
-
{ content: generateBackendUserRoutes(), path: `${backendDir}/src/api/users.ts` },
|
|
2704
|
-
{ content: generateBackendTypes(), path: `${backendDir}/src/types/index.ts` },
|
|
2705
|
-
{ content: generateBackendTypesModels(), path: `${backendDir}/src/types/models/index.ts` },
|
|
2706
|
-
{ content: generateBackendUserTypes(), path: `${backendDir}/src/types/models/userTypes.ts` },
|
|
2707
|
-
// Frontend files
|
|
2708
|
-
{ content: generateFrontendPackageJson(args), path: `${frontendDir}/package.json` },
|
|
2709
|
-
{ content: generateFrontendAppJson(args), path: `${frontendDir}/app.json` },
|
|
2710
|
-
{ content: generateFrontendTsConfig(), path: `${frontendDir}/tsconfig.json` },
|
|
2711
|
-
{ content: generateFrontendTsConfigCodegen(), path: `${frontendDir}/tsconfig.codegen.json` },
|
|
2712
|
-
{ content: generateFrontendMetroConfig(), path: `${frontendDir}/metro.config.js` },
|
|
2713
|
-
{ content: generateFrontendBiomeJsonc(), path: `${frontendDir}/biome.jsonc` },
|
|
2714
|
-
{ content: generateFrontendOpenApiConfig(), path: `${frontendDir}/openapi-config.ts` },
|
|
2715
|
-
{ content: generateFrontendGenerateSdk(), path: `${frontendDir}/scripts/generate-sdk.ts` },
|
|
2716
|
-
{ content: generateFrontendRootLayout(args), path: `${frontendDir}/app/_layout.tsx` },
|
|
2717
|
-
{ content: generateFrontendLogin(), path: `${frontendDir}/app/login.tsx` },
|
|
2718
|
-
{ content: generateFrontendSignup(), path: `${frontendDir}/app/signup.tsx` },
|
|
2719
|
-
{ content: generateFrontendNotFound(), path: `${frontendDir}/app/+not-found.tsx` },
|
|
2720
|
-
{ content: generateFrontendTabsLayout(), path: `${frontendDir}/app/(tabs)/_layout.tsx` },
|
|
2721
|
-
{ content: generateFrontendTabsIndex(args), path: `${frontendDir}/app/(tabs)/index.tsx` },
|
|
2722
|
-
{ content: generateFrontendTabsProfile(), path: `${frontendDir}/app/(tabs)/profile.tsx` },
|
|
2723
|
-
{ content: generateFrontendAdminLayout(), path: `${frontendDir}/app/(tabs)/admin/_layout.tsx` },
|
|
2724
|
-
{ content: generateFrontendAdminIndex(), path: `${frontendDir}/app/(tabs)/admin/index.tsx` },
|
|
2725
|
-
{
|
|
2726
|
-
content: generateFrontendAdminConfiguration(),
|
|
2727
|
-
path: `${frontendDir}/app/(tabs)/admin/configuration.tsx`,
|
|
2728
|
-
},
|
|
2729
|
-
{ content: generateFrontendStoreIndex(), path: `${frontendDir}/store/index.ts` },
|
|
2730
|
-
{ content: generateFrontendStoreAppState(), path: `${frontendDir}/store/appState.ts` },
|
|
2731
|
-
{ content: generateFrontendStoreErrors(), path: `${frontendDir}/store/errors.ts` },
|
|
2732
|
-
{ content: generateFrontendStoreSdk(), path: `${frontendDir}/store/sdk.ts` },
|
|
2733
|
-
{ content: generateFrontendStoreOpenApiSdk(), path: `${frontendDir}/store/openApiSdk.ts` },
|
|
2734
|
-
{ content: generateFrontendBetterAuthLib(args), path: `${frontendDir}/lib/betterAuth.ts` },
|
|
2735
|
-
{ content: generateFrontendSyncDb(args), path: `${frontendDir}/store/syncdb.ts` },
|
|
2736
|
-
{ content: generateFrontendTheme(), path: `${frontendDir}/constants/theme.ts` },
|
|
2737
|
-
{ content: generateFrontendUtilsIndex(), path: `${frontendDir}/utils/index.ts` },
|
|
2738
|
-
{ content: generateFrontendEnv(), path: `${frontendDir}/.env` },
|
|
2739
|
-
// GitHub Actions workflows
|
|
2740
|
-
{ content: generateBackendCiWorkflow(args), path: ".github/workflows/backend-ci.yml" },
|
|
2741
|
-
{ content: generateFrontendCiWorkflow(args), path: ".github/workflows/frontend-ci.yml" },
|
|
2742
|
-
];
|
|
2743
|
-
};
|
|
2744
428
|
export const handleBootstrapToolCall = (name, args) => {
|
|
2745
429
|
if (name === "terreno_bootstrap_ai_rules") {
|
|
2746
430
|
return handleBootstrapAiRulesToolCall(args);
|
|
2747
431
|
}
|
|
2748
|
-
if (name !== "terreno_bootstrap_app") {
|
|
2749
|
-
return {
|
|
2750
|
-
content: [{ text: `Unknown bootstrap tool: ${name}`, type: "text" }],
|
|
2751
|
-
};
|
|
2752
|
-
}
|
|
2753
|
-
const bootstrapArgs = args;
|
|
2754
|
-
if (!bootstrapArgs.appName || !bootstrapArgs.appDisplayName) {
|
|
2755
|
-
return {
|
|
2756
|
-
content: [
|
|
2757
|
-
{
|
|
2758
|
-
text: "Error: appName and appDisplayName are required parameters",
|
|
2759
|
-
type: "text",
|
|
2760
|
-
},
|
|
2761
|
-
],
|
|
2762
|
-
};
|
|
2763
|
-
}
|
|
2764
|
-
const files = generateAllFiles(bootstrapArgs);
|
|
2765
|
-
const fileList = files.map((f) => `- \`${f.path}\``).join("\n");
|
|
2766
|
-
const instructions = `# Bootstrap ${bootstrapArgs.appDisplayName}
|
|
2767
|
-
|
|
2768
|
-
## Files to Create
|
|
2769
|
-
|
|
2770
|
-
The following files need to be created for your new Terreno application:
|
|
2771
|
-
|
|
2772
|
-
${fileList}
|
|
2773
|
-
|
|
2774
|
-
## Instructions
|
|
2775
|
-
|
|
2776
|
-
1. **Create the project directory:**
|
|
2777
|
-
\`\`\`bash
|
|
2778
|
-
mkdir ${bootstrapArgs.appName}
|
|
2779
|
-
cd ${bootstrapArgs.appName}
|
|
2780
|
-
\`\`\`
|
|
2781
|
-
|
|
2782
|
-
2. **Create all the files listed above.** Each file's content is provided below.
|
|
2783
|
-
|
|
2784
|
-
No \`assets/\` directory is needed: \`@terreno/ui\` ships the Nunito and Titillium Web
|
|
2785
|
-
fonts it renders with, and \`app.json\` leaves \`icon\`/\`splash\`/\`favicon\` unset so Expo
|
|
2786
|
-
uses its built-in defaults. Add your own branding assets whenever you're ready and
|
|
2787
|
-
point \`app.json\` at them then.
|
|
2788
|
-
|
|
2789
|
-
3. **Install dependencies:**
|
|
2790
|
-
\`\`\`bash
|
|
2791
|
-
cd backend && bun install
|
|
2792
|
-
cd ../frontend && bun install
|
|
2793
|
-
\`\`\`
|
|
2794
|
-
|
|
2795
|
-
4. **Start MongoDB as a replica set** (required for realtime/sync):
|
|
2796
|
-
\`\`\`bash
|
|
2797
|
-
# Using Docker (single-node replica set):
|
|
2798
|
-
docker run -d --name mongo -p 27017:27017 mongo:7 --replSet rs0
|
|
2799
|
-
docker exec mongo mongosh --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"127.0.0.1:27017"}]})'
|
|
2800
|
-
\`\`\`
|
|
2801
|
-
|
|
2802
|
-
5. **Start the backend:**
|
|
2803
|
-
\`\`\`bash
|
|
2804
|
-
cd backend && bun run dev
|
|
2805
|
-
\`\`\`
|
|
2806
|
-
|
|
2807
|
-
7. **In a new terminal, seed login-ready development users:**
|
|
2808
|
-
\`\`\`bash
|
|
2809
|
-
cd backend && bun run seed
|
|
2810
|
-
# Re-run safely after seed definitions change, or use --reset to reset managed data.
|
|
2811
|
-
\`\`\`
|
|
2812
|
-
|
|
2813
|
-
8. **In a new terminal, regenerate and start the frontend:**
|
|
2814
|
-
\`\`\`bash
|
|
2815
|
-
cd frontend
|
|
2816
|
-
bun run sdk # Generate SDK from backend
|
|
2817
|
-
bun run web # Start web frontend
|
|
2818
|
-
\`\`\`
|
|
2819
|
-
|
|
2820
|
-
9. **Open http://localhost:8082** and sign in as \`test@example.com\` / \`testpassword123\`
|
|
2821
|
-
|
|
2822
|
-
## MCP Integration
|
|
2823
|
-
|
|
2824
|
-
The project is configured to use the Terreno MCP server at:
|
|
2825
|
-
\`${bootstrapArgs.mcpServerUrl || "https://mcp.terreno.flourish.health"}\`
|
|
2826
|
-
|
|
2827
|
-
This provides AI assistance with:
|
|
2828
|
-
- Generating models and routes
|
|
2829
|
-
- Creating screens and forms
|
|
2830
|
-
- Following Terreno patterns
|
|
2831
|
-
|
|
2832
|
-
---
|
|
2833
|
-
|
|
2834
|
-
## File Contents
|
|
2835
|
-
|
|
2836
|
-
`;
|
|
2837
|
-
const fileContents = files
|
|
2838
|
-
.map((f) => {
|
|
2839
|
-
const lang = getFenceLanguage(f.path);
|
|
2840
|
-
return `### \`${f.path}\`
|
|
2841
|
-
|
|
2842
|
-
\`\`\`${lang}
|
|
2843
|
-
${f.content}
|
|
2844
|
-
\`\`\`
|
|
2845
|
-
`;
|
|
2846
|
-
})
|
|
2847
|
-
.join("\n");
|
|
2848
432
|
return {
|
|
2849
|
-
content: [{ text:
|
|
433
|
+
content: [{ text: `Unknown bootstrap tool: ${name}`, type: "text" }],
|
|
2850
434
|
};
|
|
2851
435
|
};
|
|
2852
436
|
export const bootstrapPrompts = [
|
|
@@ -2863,7 +447,7 @@ export const bootstrapPrompts = [
|
|
|
2863
447
|
required: true,
|
|
2864
448
|
},
|
|
2865
449
|
],
|
|
2866
|
-
description: "Bootstrap a new Terreno
|
|
450
|
+
description: "Bootstrap a new Terreno app: instruct the assistant to run bunx create-terreno-app, then terreno_bootstrap_ai_rules",
|
|
2867
451
|
name: "terreno_bootstrap",
|
|
2868
452
|
},
|
|
2869
453
|
];
|
|
@@ -2887,15 +471,13 @@ export const handleBootstrapPromptRequest = (name, args) => {
|
|
|
2887
471
|
- **App Name** (kebab-case): ${appName}
|
|
2888
472
|
- **Display Name**: ${appDisplayName}
|
|
2889
473
|
|
|
2890
|
-
|
|
474
|
+
Scaffold the app with the published CLI (not an MCP tool):
|
|
475
|
+
|
|
476
|
+
\`\`\`bash
|
|
477
|
+
bunx create-terreno-app ${appName} --display-name "${appDisplayName}"
|
|
478
|
+
\`\`\`
|
|
2891
479
|
|
|
2892
|
-
|
|
2893
|
-
1. Create all directories and files as specified (no \`assets/\` directory is required)
|
|
2894
|
-
2. Install dependencies with \`bun install\`
|
|
2895
|
-
3. Start MongoDB
|
|
2896
|
-
4. Start the backend with \`bun run dev\`
|
|
2897
|
-
5. Generate the SDK with \`bun run sdk\` in the frontend
|
|
2898
|
-
6. Start the frontend with \`bun run web\`
|
|
480
|
+
Then follow the CLI next-step commands: install, seed, start MongoDB as a replica set, start the backend, and in another terminal run frontend \`bun run sdk\` then \`bun run web\`.
|
|
2899
481
|
|
|
2900
482
|
The application should include:
|
|
2901
483
|
- Better Auth login (email/password via \`@terreno/ui\` LoginScreen)
|
|
@@ -2906,7 +488,7 @@ The application should include:
|
|
|
2906
488
|
- Cursor rules for AI assistance
|
|
2907
489
|
- MCP integration for development assistance
|
|
2908
490
|
|
|
2909
|
-
**IMPORTANT: After
|
|
491
|
+
**IMPORTANT: After the CLI writes the app, also run the \`terreno_bootstrap_ai_rules\` tool** with the same appName and appDisplayName to set up AI coding assistant rules for Cursor, Windsurf, Claude Code, and GitHub Copilot. This will create:
|
|
2910
492
|
- AGENTS.md files for each directory
|
|
2911
493
|
- .cursorrules and .windsurfrules files
|
|
2912
494
|
- GitHub Copilot instructions
|