@tulipes/cli 0.1.0 → 0.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +11 -8
  2. package/dist/agent.d.ts +12 -0
  3. package/dist/agent.js +29 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/app-core.d.ts +8 -6
  6. package/dist/app-core.js +23 -14
  7. package/dist/app-core.js.map +1 -1
  8. package/dist/bin.js +3 -3
  9. package/dist/bin.js.map +1 -1
  10. package/dist/config-check.d.ts +8 -0
  11. package/dist/config-check.js +43 -0
  12. package/dist/config-check.js.map +1 -0
  13. package/dist/config-import.d.ts +20 -0
  14. package/dist/config-import.js +133 -0
  15. package/dist/config-import.js.map +1 -0
  16. package/dist/dev.d.ts +4 -4
  17. package/dist/dev.js +7 -10
  18. package/dist/dev.js.map +1 -1
  19. package/dist/init.d.ts +6 -11
  20. package/dist/init.js +17 -6420
  21. package/dist/init.js.map +1 -1
  22. package/dist/inspection.d.ts +9 -8
  23. package/dist/inspection.js +10 -11
  24. package/dist/inspection.js.map +1 -1
  25. package/dist/main.js +82 -11
  26. package/dist/main.js.map +1 -1
  27. package/dist/migrate.d.ts +30 -0
  28. package/dist/migrate.js +772 -0
  29. package/dist/migrate.js.map +1 -0
  30. package/dist/new-module.js +87 -46
  31. package/dist/new-module.js.map +1 -1
  32. package/dist/routes.d.ts +3 -5
  33. package/dist/routes.js +4 -6
  34. package/dist/routes.js.map +1 -1
  35. package/dist/spec.d.ts +2 -5
  36. package/dist/spec.js +13 -6
  37. package/dist/spec.js.map +1 -1
  38. package/dist/sync.d.ts +3 -11
  39. package/dist/sync.js +81 -20
  40. package/dist/sync.js.map +1 -1
  41. package/dist/template.d.ts +58 -0
  42. package/dist/template.js +244 -0
  43. package/dist/template.js.map +1 -0
  44. package/dist/update.js +4 -4
  45. package/package.json +17 -16
  46. package/templates/AGENTS.md +162 -0
  47. package/templates/CLAUDE.md +11 -118
  48. package/templates/app/.envs/.env.development +18 -0
  49. package/templates/app/.envs/.env.production +19 -0
  50. package/templates/app/.envs/.env.staging +19 -0
  51. package/templates/app/README.md +327 -0
  52. package/templates/app/app.ts +3 -0
  53. package/templates/app/config/app.config.ts +73 -0
  54. package/templates/app/docs/agent-manifest.json +724 -0
  55. package/templates/app/ecosystem.config.cjs +60 -0
  56. package/templates/app/modules/agent/controllers/agent.controllers.ts +31 -0
  57. package/templates/app/modules/agent/i18n/en.json +5 -0
  58. package/templates/app/modules/agent/i18n/fr.json +5 -0
  59. package/templates/app/modules/agent/module.acl.ts +14 -0
  60. package/templates/app/modules/agent/package.json +21 -0
  61. package/templates/app/modules/agent/routes/agent.routes.ts +29 -0
  62. package/templates/app/modules/agent/schemas/agent.schemas.ts +9 -0
  63. package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +26 -0
  64. package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +44 -0
  65. package/templates/app/modules/auth/controllers/auth.controllers.ts +312 -0
  66. package/templates/app/modules/auth/helpers/access-check.ts +42 -0
  67. package/templates/app/modules/auth/helpers/auth.ts +48 -0
  68. package/templates/app/modules/auth/helpers/documents.ts +57 -0
  69. package/templates/app/modules/auth/helpers/passport.ts +180 -0
  70. package/templates/app/modules/auth/helpers/passwords.ts +58 -0
  71. package/templates/app/modules/auth/helpers/session-cache.ts +67 -0
  72. package/templates/app/modules/auth/helpers/tokens.ts +329 -0
  73. package/templates/app/modules/auth/i18n/en.json +19 -0
  74. package/templates/app/modules/auth/i18n/fr.json +19 -0
  75. package/templates/app/modules/auth/lib/index.ts +12 -0
  76. package/templates/app/modules/auth/models/credential.model.ts +31 -0
  77. package/templates/app/modules/auth/models/revoked-token.model.ts +20 -0
  78. package/templates/app/modules/auth/models/session.model.ts +42 -0
  79. package/templates/app/modules/auth/module.acl.ts +41 -0
  80. package/templates/app/modules/auth/module.config.ts +30 -0
  81. package/templates/app/modules/auth/package.json +61 -0
  82. package/templates/app/modules/auth/routes/auth.routes.ts +94 -0
  83. package/templates/app/modules/auth/schemas/auth.schemas.ts +37 -0
  84. package/templates/app/modules/auth/tests/auth.test.ts +1079 -0
  85. package/templates/app/modules/config/controllers/config.controllers.ts +89 -0
  86. package/templates/app/modules/config/helpers/audit.ts +41 -0
  87. package/templates/app/modules/config/helpers/describe.ts +72 -0
  88. package/templates/app/modules/config/i18n/en.json +6 -0
  89. package/templates/app/modules/config/i18n/fr.json +6 -0
  90. package/templates/app/modules/config/models/config-audit.model.ts +26 -0
  91. package/templates/app/modules/config/module.acl.ts +12 -0
  92. package/templates/app/modules/config/package.json +23 -0
  93. package/templates/app/modules/config/routes/config.routes.ts +37 -0
  94. package/templates/app/modules/config/schemas/config.schemas.ts +13 -0
  95. package/templates/app/modules/config/tests/config.test.ts +126 -0
  96. package/templates/app/modules/core/bootstrap/cache.bootstrap.ts +12 -0
  97. package/templates/app/modules/core/helpers/cache/index.ts +60 -0
  98. package/templates/app/modules/core/helpers/cache/memory-cache.ts +53 -0
  99. package/templates/app/modules/core/helpers/cache/redis-cache.ts +68 -0
  100. package/templates/app/modules/core/helpers/cache/types.ts +37 -0
  101. package/templates/app/modules/core/helpers/lazy.ts +13 -0
  102. package/templates/app/modules/core/helpers/logger/index.ts +115 -0
  103. package/templates/app/modules/core/helpers/mongoose/index.ts +27 -0
  104. package/templates/app/modules/core/helpers/mongoose/plugins/base.plugin.ts +14 -0
  105. package/templates/app/modules/core/helpers/mongoose/plugins/soft-delete.plugin.ts +124 -0
  106. package/templates/app/modules/core/helpers/mongoose/plugins/timestamps.plugin.ts +13 -0
  107. package/templates/app/modules/core/helpers/mongoose/plugins/types.ts +45 -0
  108. package/templates/app/modules/core/helpers/mongoose/register.ts +20 -0
  109. package/templates/app/modules/core/lib/index.ts +34 -0
  110. package/templates/app/modules/core/module.config.ts +24 -0
  111. package/templates/app/modules/core/package.json +80 -0
  112. package/templates/app/modules/core/routes/core.routes.ts +24 -0
  113. package/templates/app/modules/core/tests/core.test.ts +19 -0
  114. package/templates/app/modules/hello/agent/hello.agent.ts +33 -0
  115. package/templates/app/modules/hello/bootstrap/seed-greetings.bootstrap.ts +27 -0
  116. package/templates/app/modules/hello/controllers/greetings.controllers.ts +43 -0
  117. package/templates/app/modules/hello/controllers/hello.controllers.ts +50 -0
  118. package/templates/app/modules/hello/helpers/greeting.ts +8 -0
  119. package/templates/app/modules/hello/i18n/en.json +8 -0
  120. package/templates/app/modules/hello/i18n/fr.json +8 -0
  121. package/templates/app/modules/hello/models/greeting.model.ts +24 -0
  122. package/templates/app/modules/hello/module.acl.ts +16 -0
  123. package/templates/app/modules/hello/module.config.ts +14 -0
  124. package/templates/app/modules/hello/package.json +24 -0
  125. package/templates/app/modules/hello/queues/hello.queues.ts +38 -0
  126. package/templates/app/modules/hello/routes/greetings.routes.ts +25 -0
  127. package/templates/app/modules/hello/routes/hello.routes.ts +44 -0
  128. package/templates/app/modules/hello/sockets/hello.sockets.ts +21 -0
  129. package/templates/app/modules/hello/tests/hello.test.ts +19 -0
  130. package/templates/app/modules/security/bootstrap/rate-limit.bootstrap.ts +8 -0
  131. package/templates/app/modules/security/helpers/hardening.ts +65 -0
  132. package/templates/app/modules/security/helpers/proxy.ts +16 -0
  133. package/templates/app/modules/security/helpers/rate-limit.ts +150 -0
  134. package/templates/app/modules/security/module.config.ts +59 -0
  135. package/templates/app/modules/security/package.json +26 -0
  136. package/templates/app/modules/security/routes/security.routes.ts +42 -0
  137. package/templates/app/modules/security/tests/security.test.ts +174 -0
  138. package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +12 -0
  139. package/templates/app/modules/settings/controllers/settings.controllers.ts +58 -0
  140. package/templates/app/modules/settings/helpers/access-lease.ts +99 -0
  141. package/templates/app/modules/settings/helpers/log-stream.ts +272 -0
  142. package/templates/app/modules/settings/helpers/log-tail.ts +253 -0
  143. package/templates/app/modules/settings/i18n/en.json +3 -0
  144. package/templates/app/modules/settings/i18n/fr.json +3 -0
  145. package/templates/app/modules/settings/module.acl.ts +12 -0
  146. package/templates/app/modules/settings/package.json +21 -0
  147. package/templates/app/modules/settings/routes/settings.routes.ts +33 -0
  148. package/templates/app/modules/settings/schemas/settings.schemas.ts +8 -0
  149. package/templates/app/modules/settings/tests/settings.test.ts +409 -0
  150. package/templates/app/modules/users/controllers/users.controllers.ts +113 -0
  151. package/templates/app/modules/users/i18n/en.json +14 -0
  152. package/templates/app/modules/users/i18n/fr.json +14 -0
  153. package/templates/app/modules/users/models/user.model.ts +18 -0
  154. package/templates/app/modules/users/module.acl.ts +10 -0
  155. package/templates/app/modules/users/module.config.ts +11 -0
  156. package/templates/app/modules/users/package.json +24 -0
  157. package/templates/app/modules/users/queues/users.queues.ts +31 -0
  158. package/templates/app/modules/users/routes/users.routes.ts +60 -0
  159. package/templates/app/modules/users/schemas/users.schemas.ts +30 -0
  160. package/templates/app/modules/users/sockets/users.sockets.ts +18 -0
  161. package/templates/app/nginx/development.conf +44 -0
  162. package/templates/app/nginx/production.conf +75 -0
  163. package/templates/app/nginx/staging.conf +51 -0
  164. package/templates/app/package.json +58 -0
  165. package/templates/app/scripts/count-greetings.ts +29 -0
  166. package/templates/app/scripts/deploy-nginx.ts +143 -0
  167. package/templates/app/scripts/list-users.ts +34 -0
  168. package/templates/app/tests/app.test.ts +28 -0
  169. package/templates/app/tests/support.ts +36 -0
  170. package/templates/app/tsconfig.json +28 -0
  171. package/templates/app/vitest.config.ts +6 -0
  172. package/templates/app/worker.ts +6 -0
  173. package/templates/claude/skills/tulipes-agent-tool/SKILL.md +83 -0
  174. package/templates/claude/skills/tulipes-boot-errors/SKILL.md +11 -5
  175. package/templates/claude/skills/tulipes-config/SKILL.md +74 -0
  176. package/templates/claude/skills/tulipes-endpoint/SKILL.md +38 -25
  177. package/templates/claude/skills/tulipes-env-variable/SKILL.md +54 -26
  178. package/templates/claude/skills/tulipes-model/SKILL.md +11 -4
  179. package/templates/claude/skills/tulipes-module/SKILL.md +11 -10
  180. package/templates/claude/skills/tulipes-queue/SKILL.md +20 -6
  181. package/templates/claude/skills/tulipes-settings/SKILL.md +49 -100
  182. package/templates/claude/skills/tulipes-spec/SKILL.md +10 -12
  183. package/templates/minimal/README.md +21 -0
  184. package/templates/minimal/app.ts +3 -0
  185. package/templates/minimal/config/app.config.ts +16 -0
  186. package/templates/minimal/modules/agent/module.acl.ts +3 -0
  187. package/templates/minimal/modules/agent/package.json +14 -0
  188. package/templates/minimal/modules/agent/routes/agent.routes.ts +20 -0
  189. package/templates/minimal/modules/core/module.acl.ts +4 -0
  190. package/templates/minimal/modules/core/package.json +21 -0
  191. package/templates/minimal/modules/hello/i18n/en.json +3 -0
  192. package/templates/minimal/modules/hello/module.acl.ts +2 -0
  193. package/templates/minimal/modules/hello/package.json +11 -0
  194. package/templates/minimal/modules/hello/routes/health.routes.ts +9 -0
  195. package/templates/minimal/modules/hello/routes/hello.routes.ts +10 -0
  196. package/templates/minimal/modules/security/package.json +15 -0
  197. package/templates/minimal/modules/security/routes/security.routes.ts +8 -0
  198. package/templates/minimal/package.json +38 -0
  199. package/templates/minimal/tests/app.test.ts +52 -0
  200. package/templates/minimal/tsconfig.json +22 -0
  201. package/templates/minimal/vitest.config.ts +5 -0
  202. package/templates/overlays/no-queues/ecosystem.config.cjs +52 -0
  203. package/templates/overlays/no-queues/modules/hello/controllers/greetings.controllers.ts +41 -0
  204. package/templates/overlays/no-queues/modules/users/controllers/users.controllers.ts +107 -0
  205. package/templates/versions.json +11 -0
  206. package/dist/minimal.d.ts +0 -2
  207. package/dist/minimal.js +0 -2200
  208. package/dist/minimal.js.map +0 -1
  209. /package/templates/{browser-auth.md → app/docs/browser-auth.md} +0 -0
@@ -0,0 +1,162 @@
1
+ # Working in this project
2
+
3
+ This is a **Tulipes** application (`@tulipes/core`) — a module-based Express
4
+ framework. It is convention-driven and deliberately fail-closed: almost
5
+ everything that can be validated at boot is, and a violation crashes the
6
+ process with a report rather than degrading quietly.
7
+
8
+ Writing plain Express here will fight the framework. Read the rule that
9
+ applies before you write code.
10
+
11
+ The `→ skill x` pointers below name a task-scoped guide in
12
+ `.claude/skills/<name>/SKILL.md`. Claude Code loads those on demand; any other
13
+ agent can read the file directly — they are plain markdown, and each one covers
14
+ one contract in depth.
15
+
16
+ ## Preset scope
17
+
18
+ Read the installed modules and package scripts before applying integration examples.
19
+ The minimal preset has core, security, hello and agent only. Auth, users,
20
+ config, settings, MongoDB, Redis, queues, sockets, shared logger/cache helpers
21
+ and PM2 examples below apply when those modules are installed (the production
22
+ preset). Minimal
23
+ requests use the framework guest identity. Grant permissions to roles the app
24
+ actually defines. Use the generated Vitest suite for changes; production tests
25
+ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
26
+
27
+ ## Non-negotiable rules
28
+
29
+ 1. **Never read `process.env` directly, and keep the environment small.**
30
+ A value is an environment variable only if boot needs it before the
31
+ database answers (connection strings, the port, log destination) or it
32
+ is a secret: declared under `tulipes.env` in the module's `package.json`, read with
33
+ `ctx.Environment.get("NAME")`. Everything else is a *setting*: declared
34
+ with a zod schema in that module's `module.config.ts`, stored in the
35
+ settings store (the `settings` collection here; a JSON file under
36
+ `config/` in an app without the Mongo store), edited through the
37
+ console, read live at `ctx.config.<module>`. → skill `tulipes-env-variable`
38
+ 2. **A feature is a module, not a layer.** Everything a feature needs —
39
+ routes, models, config, permissions, jobs — lives in one folder under
40
+ `modules/`. Never add a top-level `services/` or `middlewares/` folder.
41
+ A module's root holds exactly `package.json`, `module.config.ts` and
42
+ `module.acl.ts`; everything else has a folder (`routes/`, `controllers/`,
43
+ `models/`, `schemas/`, `helpers/`, `bootstrap/`, `queues/`, `sockets/`,
44
+ `i18n/`, `agent/`, `lib/`, `tests/`), and the boot refuses a stray root file.
45
+ → skill `tulipes-module`
46
+ 3. **Never call `mongoose.model()` yourself.** Models are declarations
47
+ picked up by the framework and read from `ctx.models`. Persisted and
48
+ wire field names are `snake_case` (`created_at`, never `createdAt`);
49
+ camelCase is for TypeScript-only names. → skill `tulipes-model`
50
+ 4. **Global middleware belongs to a sys-tier module**, not to the
51
+ entrypoint. `app.ts` stays three lines. → skill `tulipes-endpoint`
52
+ 5. **Run `pnpm sync` after touching a module's `tulipes.env`, a
53
+ settings schema in `module.config.ts`, a model file or a queue file's
54
+ `jobs` export.** It regenerates `types/tulipes.d.ts` and `.env.example`;
55
+ stale generated types are a lie. With it, `ctx.models.get("User")` is
56
+ typed from the model's schema and `queues.add("users.welcome", …)` from
57
+ the queue's job schema, so a wrong name or payload does not compile. `module.config.ts`
58
+ has exactly one export, `defineConfig({ schema })`; initialization and
59
+ cleanup belong to `bootstrap/*.bootstrap.ts`, which may return the
60
+ function that closes what it opened, and long-lived work ends on
61
+ `ctx.signal`.
62
+ 6. **Every response is `res.respond({ data, meta })`** — never `res.json`,
63
+ which ships a raw body outside the envelope. Failures are **thrown**:
64
+ `throw new HttpError(404, "notFound")`, where the message is an i18n
65
+ key and the code derives from it. Never hand-roll an error shape.
66
+ → skill `tulipes-response`
67
+ 7. **Every route declares a RAI**: put `rai({ id: "x:read" })` first in
68
+ each endpoint handler chain, including routes on a nested router.
69
+ The id is also the ACL permission, and core refuses to boot if a route
70
+ is missing one. Public is `acl.allow("guest", "x:read")`, never a
71
+ missing check. → skill `tulipes-endpoint`
72
+ 8. **Validate input with a zod schema on the RAI** (`body`, `query`,
73
+ `params`, from `zod/v4`) rather than hand-rolled `if` checks. The same
74
+ declaration generates the OpenAPI and Postman documents, so they cannot
75
+ describe a shape the endpoint rejects. → skill `tulipes-spec`
76
+ 9. **User-facing strings are keys, never literals.** `req.t("notFound")`
77
+ inside a route reads that module's `i18n/<locale>.json` — the module is
78
+ the namespace and `rai()` binds it. Never concatenate a translated
79
+ string with a variable. → skill `tulipes-i18n`
80
+ 10. **`req.auth` is who is calling** — `{ isAuthenticated, user, session }`,
81
+ resolved before any feature route and never undefined inside one. Never
82
+ re-parse the Authorization header or trust a claim from a token.
83
+ → skill `tulipes-permissions`
84
+ 11. **When auth is installed, secrets never go on the User model.** Passwords and keys belong in
85
+ the auth module's `Credential` model, which is `select: false`.
86
+ 12. **When auth is installed, a new feature module's namespace goes in BOTH the `admin` and
87
+ `sysadmin` grants** in `modules/auth/module.acl.ts`. Neither role holds
88
+ `*` — a wildcard would reach `config:write` and `settings:*` and make
89
+ the sysadmin boundary a label rather than a rule. → skills
90
+ `tulipes-config`, `tulipes-settings`
91
+ 13. **Cross-module imports use the package name** (`@app/users`,
92
+ `@app/core`), never a relative path that climbs out of the module. The
93
+ workspace boundary is what keeps modules independent, and a package
94
+ name makes the dependency visible in package.json — declare it there
95
+ and in `dependsOn`.
96
+ 14. **Every endpoint is a tool an agent can call; describe it as one.** The
97
+ `rai()` name, description and schemas are what the manifest shows, so a
98
+ bare `id` is a tool with no description. A module's `agent/*.agent.ts`
99
+ adds a summary, curates the routes and declares actions that are not
100
+ endpoints; `agent: false` on a `rai()` hides one. → skill
101
+ `tulipes-agent-tool`
102
+ 15. **A module's tests live in `modules/<name>/tests/` and boot the app
103
+ through `@tulipes/testing`**: `useTestApp({ rootDir }, { beforeAll,
104
+ afterAll })` once per file, `app.get(app.api("/x"))` with the envelope
105
+ decoded, `app.as("admin")` for a signed-in role. Never boot per test, and
106
+ never reach into another module's files from a test.
107
+
108
+ For endpoint changes, read `tulipes-endpoint`: native routers live in
109
+ `routes/*.routes.ts`, plain `(ctx) => Router` functions: build the router from
110
+ the context, return it for the configured prefix, and read models, settings
111
+ and the environment inside handlers, never while building — `tulipes routes`
112
+ and `tulipes spec` build every router with nothing acquired. Global
113
+ middleware that needs the environment is built on its first request with
114
+ `lazy()` from `@app/core`.
115
+
116
+ ## Where things go
117
+
118
+ | Decision | Location |
119
+ |---|---|
120
+ | Connection string, secret, or anything boot needs before the database answers | `tulipes.env` in that module's `package.json` |
121
+ | Policy shared by ≥2 modules and the same everywhere (pagination, locales) | `config/app.config.ts` |
122
+ | A value an operator may change at runtime (limits, modes, toggles, lifetimes) | a field in that module's `module.config.ts` schema, read at `ctx.config.<module>` |
123
+ | Opening a client, seeding rows, ensuring indexes, and closing them again | that module's `bootstrap/*.bootstrap.ts`, returning the cleanup |
124
+ | A string a user will read | that module's `i18n/<locale>.json`, reached with `req.t` |
125
+ | The shape of a request body/query | a zod schema on the route's `rai()` |
126
+ | Middleware for every request | a sys-tier module's routes file |
127
+ | Middleware for one feature's routes | that module's own `Router` |
128
+ | Helper used by ≥2 modules | the sys core module, exported from its `lib/index.ts` and imported as `@app/core` |
129
+ | Helper used by one module | that module's `helpers/` |
130
+ | One-off task (backfill, export, admin chore) | `scripts/`, booted with `mode: "script"` |
131
+ | Anything you would `console.log` | `moduleLogger(ctx.Environment, "<module>", ctx.mode)` from `@app/core` |
132
+
133
+ ## Boot order (why load order matters)
134
+
135
+ `sys` modules load before `app` modules; within a tier, lower `priority`
136
+ first; then `dependsOn` topological order. Declarations → models →
137
+ settings → translations → ACL → bootstrap → queues → routes. If code needs
138
+ something another module provides, declare `dependsOn` — do not rely on
139
+ luck.
140
+
141
+ ## Commands
142
+
143
+ | Command | Use |
144
+ |---|---|
145
+ | `pnpm dev` | run the backend process (regenerates types first) |
146
+ | `pnpm worker` | run the queue-consumer process |
147
+ | `pnpm sync` | regenerate `types/tulipes.d.ts` + `.env.example` |
148
+ | `pnpm tulipes agent manifest [--role r]` | print every tool the app exposes to agents, offline |
149
+ | `pnpm test` | every `tests/` and `modules/*/tests/` file; each boots the app once through `@tulipes/testing` |
150
+ | `pnpm check` | validate the environment without booting |
151
+ | `pnpm typecheck` | typecheck the whole app |
152
+ | `pnpm tulipes new module <name>` | scaffold a module |
153
+ | `tulipes update` | upgrade the global cli and the framework in every manifest |
154
+ | `pnpm script scripts/x.ts` | run a one-off script against the real app |
155
+
156
+ ## Verifying your work
157
+
158
+ A change is not done until the app boots. Run `pnpm typecheck`, then
159
+ `pnpm dev` and confirm the startup banner lists what you added (modules,
160
+ queues, sockets, roles). Boot failures print an aggregate report naming
161
+ every problem at once — read all of it before fixing anything. → skill
162
+ `tulipes-boot-errors`
@@ -1,120 +1,13 @@
1
1
  # Working in this project
2
2
 
3
- This is a **Tulipes** application (`@tulipes/core`) — a module-based Express
4
- framework. It is convention-driven and deliberately fail-closed: almost
5
- everything that can be validated at boot is, and a violation crashes the
6
- process with a report rather than degrading quietly.
7
-
8
- Writing plain Express here will fight the framework. Read the rule that
9
- applies before you write code.
10
-
11
- ## Preset scope
12
-
13
- Read the installed modules and package scripts before applying integration examples.
14
- The minimal preset has core, security and hello only. Auth, users, settings,
15
- MongoDB, Redis, queues, sockets, shared logger/cache helpers and PM2 examples
16
- below apply when those modules are installed (the production preset). Minimal
17
- requests use the framework guest identity. Grant permissions to roles the app
18
- actually defines. Use the generated Vitest suite for changes; production tests
19
- require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
20
-
21
- ## Non-negotiable rules
22
-
23
- 1. **Never read `process.env` directly.** Every configuration value is
24
- declared in a module's `meta.variables.json` and read with
25
- `ctx.Environment.get("NAME")`. Reading an undeclared name throws on
26
- purpose. → skill `tulipes-env-variable`
27
- 2. **A feature is a module, not a layer.** Everything a feature needs —
28
- routes, models, config, permissions, jobs — lives in one folder under
29
- `modules/`. Never add a top-level `services/` or `middlewares/` folder.
30
- → skill `tulipes-module`
31
- 3. **Never call `mongoose.model()` yourself.** Models are declarations
32
- picked up by the framework and read from `ctx.models`. Persisted and
33
- wire field names are `snake_case` (`created_at`, never `createdAt`);
34
- camelCase is for TypeScript-only names. → skill `tulipes-model`
35
- 4. **Global middleware belongs to a sys-tier module**, not to the
36
- entrypoint. `app.ts` stays three lines. → skill `tulipes-endpoint`
37
- 5. **Run `yarn sync` after touching any `meta.variables.json` or
38
- `module.config.ts`.** It regenerates `types/config.d.ts` and
39
- `.env.example`; stale generated types are a lie.
40
- 6. **Every response is `res.respond({ data, meta })`** — never `res.json`,
41
- which ships a raw body outside the envelope. Failures are **thrown**:
42
- `throw new HttpError(404, "notFound")`, where the message is an i18n
43
- key and the code derives from it. Never hand-roll an error shape.
44
- → skill `tulipes-response`
45
- 7. **Every route declares a RAI**: put `rai({ id: "x:read" })` first in
46
- each endpoint handler chain, including routes inside `defineRoutes()`.
47
- The id is also the ACL permission, and core refuses to boot if a route
48
- is missing one. Public is `acl.allow("guest", "x:read")`, never a
49
- missing check. → skill `tulipes-endpoint`
50
- 8. **Validate input with a zod schema on the RAI** (`body`, `query`,
51
- `params`, from `zod/v4`) rather than hand-rolled `if` checks. The same
52
- declaration generates the OpenAPI and Postman documents, so they cannot
53
- describe a shape the endpoint rejects. → skill `tulipes-spec`
54
- 9. **User-facing strings are keys, never literals.** `req.t("notFound")`
55
- inside a route reads that module's `i18n/<locale>.json` — the module is
56
- the namespace and `rai()` binds it. Never concatenate a translated
57
- string with a variable. → skill `tulipes-i18n`
58
- 10. **`req.auth` is who is calling** — `{ isAuthenticated, user, session }`,
59
- resolved before any feature route and never undefined inside one. Never
60
- re-parse the Authorization header or trust a claim from a token.
61
- → skill `tulipes-permissions`
62
- 11. **When auth is installed, secrets never go on the User model.** Passwords and keys belong in
63
- the auth module's `Credential` model, which is `select: false`.
64
- 12. **When auth is installed, a new feature module's namespace goes in BOTH the `admin` and
65
- `sysadmin` grants** in `modules/auth/module.acl.ts`. Neither role holds
66
- `*` — a wildcard would reach `settings:*` and make the sysadmin
67
- boundary a label rather than a rule. → skill `tulipes-settings`
68
- 13. **Cross-module imports use the package name** (`@app/users`,
69
- `@app/core`), never a relative path that climbs out of the module. The
70
- workspace boundary is what keeps modules independent, and a package
71
- name makes the dependency visible in package.json — declare it there
72
- and in `dependsOn`.
73
-
74
- For endpoint changes, read `tulipes-endpoint`: native routers live in
75
- `routes/*.routes.ts`. Use `defineRoutes()` and `runtime()` for offline inspection;
76
- keep shared middleware and param callbacks beside their endpoints.
77
-
78
- ## Where things go
79
-
80
- | Decision | Location |
81
- |---|---|
82
- | Value differs per deployment (URL, secret, toggle) | that module's `meta.variables.json` |
83
- | Policy shared by ≥2 modules (pagination, body limit) | `config/app.config.ts` |
84
- | Config for one feature | that module's `module.config.ts` |
85
- | A string a user will read | that module's `i18n/<locale>.json`, reached with `req.t` |
86
- | The shape of a request body/query | a zod schema on the route's `rai()` |
87
- | Middleware for every request | a sys-tier module's routes file |
88
- | Middleware for one feature's routes | that module's own `Router` |
89
- | Helper used by ≥2 modules | the sys core module, exported from its `index.ts` and imported as `@app/core` |
90
- | Helper used by one module | that module's `helpers/` |
91
- | One-off task (backfill, export, admin chore) | `scripts/`, booted with `mode: "script"` |
92
- | Anything you would `console.log` | `moduleLogger(ctx.Environment, "<module>", ctx.mode)` from `@app/core` |
93
-
94
- ## Boot order (why load order matters)
95
-
96
- `sys` modules load before `app` modules; within a tier, lower `priority`
97
- first; then `dependsOn` topological order. Config → translations → ACL →
98
- database → queues → routes. If code needs something another module
99
- provides, declare `dependsOn` — do not rely on luck.
100
-
101
- ## Commands
102
-
103
- | Command | Use |
104
- |---|---|
105
- | `yarn dev` | run the backend process (regenerates types first) |
106
- | `yarn worker` | run the queue-consumer process |
107
- | `yarn sync` | regenerate `types/config.d.ts` + `.env.example` |
108
- | `yarn check` | validate the environment without booting |
109
- | `yarn typecheck` | typecheck the whole app |
110
- | `yarn tulipes new module <name>` | scaffold a module |
111
- | `tulipes update` | upgrade the global cli and the framework in every manifest |
112
- | `yarn script scripts/x.ts` | run a one-off script against the real app |
113
-
114
- ## Verifying your work
115
-
116
- A change is not done until the app boots. Run `yarn typecheck`, then
117
- `yarn dev` and confirm the startup banner lists what you added (modules,
118
- queues, sockets, roles). Boot failures print an aggregate report naming
119
- every problem at once — read all of it before fixing anything. → skill
120
- `tulipes-boot-errors`
3
+ The project instructions live in **[AGENTS.md](./AGENTS.md)** — the
4
+ vendor-neutral file every coding agent reads. Read it before writing code
5
+ here; it holds the non-negotiable rules, where each kind of code belongs, the
6
+ boot order and the commands.
7
+
8
+ This file exists so Claude Code picks that up automatically. The other
9
+ Claude-specific piece is `.claude/skills/`: task-scoped guides — module,
10
+ environment variable, endpoint, response, model, queue, socket, permissions,
11
+ settings (the config module and the log stream), i18n, spec, agent tools and
12
+ reading boot errors — loaded on demand when a task matches one. AGENTS.md
13
+ points at them by name.
@@ -0,0 +1,18 @@
1
+ # Development. NOT committed — .gitignore excludes .envs/, because a
2
+ # file holding working credentials does not belong in a repository.
3
+ # The committed contract is .env.example at the root: copy it here and
4
+ # fill it in. Real deployments inject variables through the container
5
+ # environment, which always wins over this file.
6
+
7
+ PORT=3000
8
+ LOG_LEVEL=debug
9
+
10
+ MONGO_URI=mongodb://127.0.0.1:27017/tulipes-example
11
+ REDIS_URL=redis://127.0.0.1:6379
12
+
13
+ # Auth. Separate keys, so leaking one cannot mint the other.
14
+ JWT_ACCESS_SECRET=dev-only-access-secret-change-me
15
+ JWT_REFRESH_SECRET=dev-only-refresh-secret-change-me
16
+ AUTH_ADMIN_EMAIL=admin@example.com
17
+ AUTH_ADMIN_PASSWORD=dev-admin-password
18
+
@@ -0,0 +1,19 @@
1
+ # Production. NOT committed — .gitignore excludes .envs/. Recreate it
2
+ # on the deployment host from .env.example, or leave it out entirely
3
+ # and inject everything through the container environment.
4
+ #
5
+ # The blanks below are intentional. Real deployments inject MONGO_URI,
6
+ # REDIS_URL and any secret through the container environment, which
7
+ # always wins over this file. Booting without them fails closed with a
8
+ # report naming every missing variable — run `pnpm check` to see it
9
+ # without starting the app.
10
+
11
+ PORT=3000
12
+ LOG_LEVEL=info
13
+
14
+ # MONGO_URI=
15
+ # REDIS_URL=
16
+ # JWT_ACCESS_SECRET=
17
+ # JWT_REFRESH_SECRET=
18
+ # AUTH_ADMIN_EMAIL=
19
+ # AUTH_ADMIN_PASSWORD=
@@ -0,0 +1,19 @@
1
+ # Staging. NOT committed — .gitignore excludes .envs/. Recreate it on
2
+ # the deployment host from .env.example, or leave it out entirely and
3
+ # inject everything through the container environment.
4
+ #
5
+ # The blanks below are intentional. Real deployments inject MONGO_URI,
6
+ # REDIS_URL and any secret through the container environment, which
7
+ # always wins over this file. Booting without them fails closed with a
8
+ # report naming every missing variable — run `pnpm check` to see it
9
+ # without starting the app.
10
+
11
+ PORT=3000
12
+ LOG_LEVEL=info
13
+
14
+ # MONGO_URI=
15
+ # REDIS_URL=
16
+ # JWT_ACCESS_SECRET=
17
+ # JWT_REFRESH_SECRET=
18
+ # AUTH_ADMIN_EMAIL=
19
+ # AUTH_ADMIN_PASSWORD=