@tulipes/cli 0.2.0-rc.2 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/agent.d.ts +5 -5
  2. package/dist/config-check.js +1 -1
  3. package/dist/env-check.d.ts +5 -3
  4. package/dist/env-check.js +26 -19
  5. package/dist/env-check.js.map +1 -1
  6. package/dist/main.js +83 -19
  7. package/dist/main.js.map +1 -1
  8. package/dist/migrate-0.12-env.d.ts +36 -0
  9. package/dist/migrate-0.12-env.js +250 -0
  10. package/dist/migrate-0.12-env.js.map +1 -0
  11. package/dist/migrate-0.12-scaffold.d.ts +1 -0
  12. package/dist/migrate-0.12-scaffold.js +197 -0
  13. package/dist/migrate-0.12-scaffold.js.map +1 -0
  14. package/dist/migrate-0.12.d.ts +47 -0
  15. package/dist/migrate-0.12.js +210 -0
  16. package/dist/migrate-0.12.js.map +1 -0
  17. package/dist/migrate.d.ts +3 -1
  18. package/dist/migrate.js +44 -5
  19. package/dist/migrate.js.map +1 -1
  20. package/dist/new-module.d.ts +7 -0
  21. package/dist/new-module.js +35 -16
  22. package/dist/new-module.js.map +1 -1
  23. package/dist/roles.d.ts +15 -0
  24. package/dist/roles.js +54 -0
  25. package/dist/roles.js.map +1 -0
  26. package/dist/routes.d.ts +7 -2
  27. package/dist/routes.js +19 -10
  28. package/dist/routes.js.map +1 -1
  29. package/dist/spec.d.ts +2 -0
  30. package/dist/spec.js +13 -11
  31. package/dist/spec.js.map +1 -1
  32. package/dist/sync.js +66 -53
  33. package/dist/sync.js.map +1 -1
  34. package/dist/update.js +1 -1
  35. package/dist/update.js.map +1 -1
  36. package/package.json +4 -4
  37. package/templates/AGENTS.md +41 -29
  38. package/templates/app/.envs/.env.development +2 -1
  39. package/templates/app/.envs/.env.production +2 -2
  40. package/templates/app/.envs/.env.staging +2 -2
  41. package/templates/app/README.md +44 -29
  42. package/templates/app/config/app.config.ts +60 -7
  43. package/templates/app/docs/agent-manifest.json +8 -681
  44. package/templates/app/modules/agent/controllers/{agent.controllers.ts → web.agent.controllers.ts} +4 -5
  45. package/templates/app/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +4 -6
  46. package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +20 -12
  47. package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +4 -2
  48. package/templates/app/modules/auth/controllers/{auth.controllers.ts → web.auth.controllers.ts} +1 -1
  49. package/templates/app/modules/auth/helpers/passport.ts +1 -1
  50. package/templates/app/modules/auth/helpers/session-cache.ts +3 -3
  51. package/templates/app/modules/auth/helpers/tokens.ts +3 -5
  52. package/templates/app/modules/auth/module.acl.ts +31 -9
  53. package/templates/app/modules/auth/module.config.ts +5 -3
  54. package/templates/app/modules/auth/package.json +10 -10
  55. package/templates/app/modules/auth/routes/{auth.routes.ts → web.auth.routes.ts} +15 -10
  56. package/templates/app/modules/auth/tests/auth.test.ts +1 -1
  57. package/templates/app/modules/config/controllers/{config.controllers.ts → bo.config.controllers.ts} +3 -2
  58. package/templates/app/modules/config/helpers/audit.ts +1 -1
  59. package/templates/app/modules/config/helpers/describe.ts +35 -26
  60. package/templates/app/modules/config/module.acl.ts +1 -1
  61. package/templates/app/modules/config/routes/{config.routes.ts → bo.config.routes.ts} +5 -5
  62. package/templates/app/modules/config/tests/config.test.ts +14 -6
  63. package/templates/app/modules/core/helpers/cache/index.ts +8 -7
  64. package/templates/app/modules/core/helpers/lazy.ts +3 -3
  65. package/templates/app/modules/core/helpers/logger/index.ts +24 -16
  66. package/templates/app/modules/core/module.config.ts +5 -3
  67. package/templates/app/modules/core/package.json +46 -19
  68. package/templates/app/modules/core/routes/core.routes.ts +4 -4
  69. package/templates/app/modules/hello/agent/hello.agent.ts +18 -14
  70. package/templates/app/modules/hello/controllers/{hello.controllers.ts → web/hello.controllers.ts} +1 -1
  71. package/templates/app/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +4 -8
  72. package/templates/app/modules/hello/helpers/greetings.ts +14 -0
  73. package/templates/app/modules/hello/module.acl.ts +12 -7
  74. package/templates/app/modules/hello/queues/hello.queues.ts +1 -1
  75. package/templates/app/modules/hello/routes/{greetings.routes.ts → web/hello.greetings.routes.ts} +4 -4
  76. package/templates/app/modules/hello/routes/{hello.routes.ts → web/hello.routes.ts} +14 -8
  77. package/templates/app/modules/security/helpers/rate-limit.ts +3 -2
  78. package/templates/app/modules/security/routes/security.routes.ts +4 -4
  79. package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +2 -2
  80. package/templates/app/modules/settings/controllers/{settings.controllers.ts → bo.settings.controllers.ts} +7 -6
  81. package/templates/app/modules/settings/helpers/log-stream.ts +6 -5
  82. package/templates/app/modules/settings/routes/{settings.routes.ts → bo.settings.routes.ts} +3 -4
  83. package/templates/app/modules/settings/tests/settings.test.ts +21 -25
  84. package/templates/app/modules/users/controllers/{users.controllers.ts → bo.users.controllers.ts} +5 -55
  85. package/templates/{overlays/no-queues/modules/users/controllers/users.controllers.ts → app/modules/users/controllers/web.users.controllers.ts} +4 -42
  86. package/templates/app/modules/users/module.acl.ts +4 -4
  87. package/templates/app/modules/users/queues/users.queues.ts +1 -1
  88. package/templates/app/modules/users/routes/bo.users.routes.ts +39 -0
  89. package/templates/app/modules/users/routes/web.users.routes.ts +45 -0
  90. package/templates/app/nginx/production.conf +1 -1
  91. package/templates/app/scripts/deploy-nginx.ts +3 -2
  92. package/templates/app/tests/support.ts +4 -3
  93. package/templates/claude/skills/tulipes-agent-tool/SKILL.md +49 -34
  94. package/templates/claude/skills/tulipes-boot-errors/SKILL.md +13 -7
  95. package/templates/claude/skills/tulipes-config/SKILL.md +6 -4
  96. package/templates/claude/skills/tulipes-endpoint/SKILL.md +36 -19
  97. package/templates/claude/skills/tulipes-env-variable/SKILL.md +62 -37
  98. package/templates/claude/skills/tulipes-i18n/SKILL.md +20 -1
  99. package/templates/claude/skills/tulipes-module/SKILL.md +10 -5
  100. package/templates/claude/skills/tulipes-permissions/SKILL.md +123 -35
  101. package/templates/claude/skills/tulipes-queue/SKILL.md +3 -2
  102. package/templates/claude/skills/tulipes-settings/SKILL.md +9 -8
  103. package/templates/claude/skills/tulipes-spec/SKILL.md +12 -2
  104. package/templates/minimal/config/app.config.ts +13 -3
  105. package/templates/minimal/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +2 -2
  106. package/templates/minimal/modules/core/module.acl.ts +7 -1
  107. package/templates/minimal/modules/core/package.json +4 -4
  108. package/templates/minimal/modules/hello/module.acl.ts +1 -1
  109. package/templates/minimal/modules/hello/routes/{health.routes.ts → web.hello.health.routes.ts} +1 -1
  110. package/templates/minimal/modules/hello/routes/{hello.routes.ts → web.hello.routes.ts} +1 -1
  111. package/templates/overlays/no-queues/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +1 -1
  112. package/templates/overlays/no-queues/modules/users/controllers/bo.users.controllers.ts +57 -0
  113. package/templates/versions.json +8 -8
  114. package/templates/app/modules/users/routes/users.routes.ts +0 -60
@@ -10,9 +10,10 @@ pnpm install
10
10
  pnpm dev # boot the backend process → http://localhost:3000/api/v1/hello
11
11
  ```
12
12
 
13
- Needs mongo and redis running — see `MONGO_URI` and `REDIS_URL` in
14
- `.envs/.env.development`. Run `pnpm worker` in a second terminal to
15
- consume queued jobs.
13
+ Needs mongo and redis running — see `MONGO_URI` (or the `MONGO_HOST`,
14
+ `MONGO_USERNAME`, `MONGO_PASSWORD` and `MONGO_DBNAME` parts) and
15
+ `REDIS_URL` in `.envs/.env.development`. Run `pnpm worker` in a second
16
+ terminal to consume queued jobs.
16
17
 
17
18
  `.envs/` is git-ignored: those files hold working credentials. On a
18
19
  fresh clone there is nothing in it, so copy `.env.example` — which is
@@ -23,7 +24,7 @@ declared variables — and fill it in:
23
24
  pnpm sync # refresh .env.example and types
24
25
  mkdir -p .envs # git does not track empty dirs
25
26
  cp .env.example .envs/.env.development
26
- pnpm check # validate it without booting
27
+ pnpm check # resolve it without booting, and see where each value lands
27
28
  ```
28
29
 
29
30
  ## Verify
@@ -33,7 +34,8 @@ services: `TEST_MONGO_URI=mongodb://127.0.0.1:27017/tulipes_tests TEST_REDIS_URL
33
34
  Use test-only data: the suite creates users and sessions. `pnpm spec` writes
34
35
  OpenAPI/Postman documents offline, without services or runtime secrets.
35
36
  Change the shared API prefix in `config/routes.ts`. Runtime middleware stays
36
- with endpoints in `routes/*.routes.ts`, plain functions of the context.
37
+ with endpoints in `routes/`, plain functions of the context, named
38
+ scope-first (`routes/web.users.routes.ts`, `routes/bo.users.routes.ts`).
37
39
  Staging/production boot rejects development credentials and equal JWT keys.
38
40
  Auth, settings and deployment examples are application policy you own.
39
41
 
@@ -44,11 +46,11 @@ before writing your own:
44
46
 
45
47
  | File | Contract |
46
48
  |---|---|
47
- | `package.json` `tulipes.env` | the module's environment contract |
49
+ | `package.json` `tulipes.env` | the module's environment values, read at `ctx.config.hello.<name>` (hello declares none) |
48
50
  | `module.config.ts` | `defineConfig({ schema })`: this module's settings |
49
- | `module.acl.ts` | permission grants on `hello:*` |
50
- | `routes/*.routes.ts` | native routers, shared middleware and RAI schemas |
51
- | `controllers/` | the work; constructed from the context while routes build |
51
+ | `module.acl.ts` | permission grants on `hello:web:*` ids, with prefix wildcards |
52
+ | `routes/web/*.routes.ts` | native routers, shared middleware and RAI schemas; one folder per scope |
53
+ | `controllers/web/` | the work; constructed from the context while routes build |
52
54
  | `helpers/` | pure logic, no framework |
53
55
  | `i18n/<locale>.json` | this module's strings — the module is the namespace |
54
56
  | `sockets/*.sockets.ts` | a claimed Socket.IO namespace | <!-- feature:sockets -->
@@ -64,7 +66,9 @@ before writing your own:
64
66
  | `pnpm worker` | run the queue-consumer process (needs a queue first) | <!-- feature:queues -->
65
67
  | `pnpm sync` | regenerate `types/tulipes.d.ts` + `.env.example` |
66
68
  | `pnpm check` | validate env against every module contract — CI gate |
67
- | `pnpm tulipes new module <name>` | scaffold a module |
69
+ | `pnpm tulipes new module <name>` | scaffold a module (`--scope bo` for the back office) |
70
+ | `pnpm tulipes routes` | every endpoint and the roles that reach it (`--scope bo` for one surface) |
71
+ | `pnpm tulipes roles` | every role, its description and the grants it holds |
68
72
  | `pnpm tulipes update` | upgrade the framework everywhere it is declared |
69
73
 
70
74
  ## Authentication
@@ -75,21 +79,29 @@ becomes the **guest** caller, which is what makes a public endpoint
75
79
  an ordinary ACL grant instead of a second mechanism:
76
80
 
77
81
  ```ts
78
- acl.allow("guest", "auth:login"); // that is why login is reachable
82
+ acl.allow("guest", "auth:web:login"); // that is why login is reachable
79
83
  ```
80
84
 
81
85
  Guarding is the RAI itself — its `id` IS the permission, so there is
82
- no second call to forget. 401 means "sign in"; 403 means "signed in,
83
- still not allowed":
86
+ no second call to forget. An id is always `module:scope:resource[:extra]`:
87
+ `users:web:list`, `users:bo:list:deleted`. 401 means "sign in"; 403 means
88
+ "signed in, still not allowed":
84
89
 
85
90
  ```ts
86
91
  router.get(
87
92
  `${base}/invoices`,
88
- rai({ id: "invoices:read", name: "List invoices" }),
93
+ rai({ id: "billing:web:invoices", name: "List invoices" }),
89
94
  billing.list(),
90
95
  );
91
96
  ```
92
97
 
98
+ Roles are defined once in `modules/auth/module.acl.ts`, each with a
99
+ description and any extra the app reads back (`level` here):
100
+
101
+ ```ts
102
+ acl.defineRole("admin", { description: "Runs the back office", level: 2 });
103
+ ```
104
+
93
105
  A route that declares no RAI would be reachable by guest, so core
94
106
  scans the router stack after mounting and refuses to boot when it
95
107
  finds one. Run `pnpm tulipes routes` to see every endpoint with the
@@ -139,8 +151,9 @@ TTL indexes.
139
151
  ## The console
140
152
 
141
153
  `modules/config` is the operator's console: `GET /api/v1/config`
142
- describes every module — manifest, declared variables (names, types and
143
- sources; a secret's value never), settings with their schema, live values
154
+ describes every module — manifest, environment values (config name,
155
+ variable, type and source; a secret's value never), settings with their
156
+ schema, live values
144
157
  and stored version, endpoints with the roles that reach them — and
145
158
  `PATCH /api/v1/config/:module` changes a module's settings: validated by
146
159
  its schema, refused on a stale version, audited (names, never values)
@@ -149,7 +162,7 @@ writing needs `sysadmin`. Settings live in MongoDB (`settings` and
149
162
  `settings_history`) because the root `package.json` declares
150
163
  `@tulipes/mongoose/settings`; the request pipeline — proxy trust, CORS,
151
164
  HSTS, the body limit, the rate limits — is configured from
152
- `config.security` this way, so none of it is an environment variable.
165
+ `config.security` this way, so none of it is an environment value.
153
166
 
154
167
  ## Logging
155
168
 
@@ -158,7 +171,7 @@ The logger lives in the core module and is shared by every other one:
158
171
  ```ts
159
172
  import { moduleLogger } from "@app/core";
160
173
 
161
- const log = moduleLogger(ctx.Environment, "billing", ctx.mode);
174
+ const log = moduleLogger(ctx, "billing");
162
175
  log.info({ invoiceId }, "invoice sent");
163
176
  ```
164
177
 
@@ -173,9 +186,10 @@ Files carry the process mode — `app-backend.log`, `app-worker.log` —
173
186
  because backend and worker run at the same time and two processes
174
187
  rotating one file will race.
175
188
 
176
- The destinations are environment, because the logger exists before the
177
- settings store answers: `LOG_LEVEL`, `LOG_CONSOLE`, `LOG_FILE` and
178
- `LOG_DIR`, declared in `modules/core/package.json`. The file names and
189
+ The destinations are environment values, because the logger exists before
190
+ the settings store answers: `LOG_LEVEL`, `LOG_CONSOLE`, `LOG_FILE` and
191
+ `LOG_DIR`, declared in `modules/core/package.json` and read as
192
+ `config.core.logLevel`, `logConsole`, `logFile` and `logDir`. The file names and
179
193
  the rotation (daily or 20m, whichever trips first; fourteen rotated
180
194
  files kept) never needed to differ per deployment, so they are the
181
195
  `LOG_FILES` constants in `modules/core/helpers/logger/`. A `sysadmin`
@@ -268,8 +282,8 @@ redis hold the event loop open, so a script that forgets
268
282
  `nginx/{development,staging,production}.conf` are templates with
269
283
  `{{DOMAIN}}` and `{{PORT}}` placeholders. The deploy script renders
270
284
  the one matching `APP_ENV`, using that same environment's
271
- the `core.publicDomain` setting and `PORT`, so template, domain and port can never
272
- disagree:
285
+ `core.publicDomain` setting and the port `config/app.config.ts` wired from
286
+ `PORT`, so template, domain and port can never disagree:
273
287
 
274
288
  ```sh
275
289
  APP_ENV=production pnpm script scripts/deploy-nginx.ts --dry-run
@@ -309,12 +323,13 @@ PM2 remembers the environment a process was started with:
309
323
 
310
324
  `.envs/` holds one file per mode and is **git-ignored** — those files
311
325
  carry working credentials. What is committed is `.env.example`,
312
- generated by `pnpm sync`, naming every variable any module declares:
313
- a clone copies it into `.envs/.env.<mode>` and fills it in. Servers
314
- usually skip the file entirely and inject variables through the
315
- container environment, which always wins over it. `pnpm check`
316
- validates the current environment without booting, so a deploy can
317
- fail on configuration before it fails on traffic.
326
+ generated by `pnpm sync`, naming every variable any module declares
327
+ and the `config.<module>.<name>` it lands on: a clone copies it into
328
+ `.envs/.env.<mode>` and fills it in. Servers usually skip the file
329
+ entirely and inject variables through the container environment, which
330
+ always wins over it. `pnpm check` resolves the current environment
331
+ without booting, so a deploy can fail on configuration before it fails
332
+ on traffic.
318
333
 
319
334
  PM2 is a devDependency so the scripts work straight after install;
320
335
  servers usually install it globally instead (`npm i -g pm2`), in
@@ -3,24 +3,35 @@ import type { AppConfigFn } from "@tulipes/core/boot";
3
3
  /**
4
4
  * ── The app-wide config seed ──────────────────────────────────────────
5
5
  *
6
- * Runs FIRST in boot phase 6, before any module.config.ts factory, and
6
+ * Runs FIRST in boot phase 4, before any module's settings are loaded, and
7
7
  * whatever it returns becomes the base of the global config. Modules read
8
8
  * it through `ctx.config.<key>`.
9
9
  *
10
+ * It receives `env`: every module's environment values, resolved from the
11
+ * container, `.envs/.env.<APP_ENV>` and the defaults each module declares
12
+ * in its package.json `tulipes.env` — `env.core.port`, `env.auth.adminEmail`
13
+ * — typed by `pnpm sync`. This is the one place the environment is wired
14
+ * to what the framework and the providers read: `server.port` for core,
15
+ * `mongoose.*` for @tulipes/mongoose, `bullmq.redisUrl` for @tulipes/bullmq.
16
+ * Modules never need this wiring: each reads its own values as
17
+ * `ctx.config.<module>.<name>`, beside its settings.
18
+ *
10
19
  * Add a key here when two or more modules share a value. Config belonging
11
20
  * to ONE feature goes in that module's `module.config.ts` instead, where
12
21
  * `pnpm sync` types it for you — that is where `users.maxSessions` and
13
22
  * `users.signupMode` live. A value that differs per deployment belongs in
14
- * neither: declare it in a module's `meta.variables.json` and read it with
15
- * `Environment.get`, the way MONGO_URI, CORS_ORIGINS and BODY_LIMIT do.
23
+ * neither: declare it under `tulipes.env` in a module's package.json and
24
+ * read it as `ctx.config.<module>.<name>`, the way MONGO_URI and REDIS_URL
25
+ * are declared by the core module.
16
26
  *
17
- * The framework reads exactly one field of this object: `app.name`, which
18
- * titles the startup banner. Everything else is yours.
27
+ * Under inspection (`tulipes routes`, `tulipes spec`) a value that is
28
+ * missing or invalid is `undefined` rather than an error, so this file
29
+ * only wires values and never requires one.
19
30
  */
20
31
 
21
32
  // `satisfies` (not a type annotation) so TypeScript keeps the exact shape
22
33
  // of this object for the augmentation at the bottom of the file.
23
- const appConfig = (() => ({
34
+ const appConfig = ((env) => ({
24
35
  app: {
25
36
  name: "tulipes-example",
26
37
  },
@@ -34,6 +45,31 @@ const appConfig = (() => ({
34
45
  prefix: "/api/v1",
35
46
  },
36
47
 
48
+ /** What core listens on. */
49
+ server: {
50
+ port: env.core.port,
51
+ },
52
+
53
+ /**
54
+ * How @tulipes/mongoose reaches MongoDB. A full MONGO_URI wins — it is the
55
+ * only way to name a replica set, an Atlas cluster or driver options;
56
+ * otherwise the host, credentials and database name are handed to the
57
+ * driver separately, so nothing is URL-encoded by hand.
58
+ */
59
+ mongoose: env.core.mongodbUri
60
+ ? { uri: env.core.mongodbUri }
61
+ : {
62
+ uri: env.core.mongodbHost,
63
+ user: env.core.mongodbUsername,
64
+ pass: env.core.mongodbPassword,
65
+ dbName: env.core.mongodbDbName,
66
+ },
67
+
68
+ /** How @tulipes/bullmq reaches Redis. */
69
+ bullmq: {
70
+ redisUrl: env.core.redisUrl,
71
+ },
72
+
37
73
  /**
38
74
  * The languages this app speaks. Every module owns its own strings in
39
75
  * `modules/<name>/i18n/<locale>.json` — the module is the namespace —
@@ -50,6 +86,21 @@ const appConfig = (() => ({
50
86
  supportedLocales: ["en", "fr"],
51
87
  },
52
88
 
89
+ /**
90
+ * How instants are read. Dates are stored and returned as UTC; a zone is
91
+ * what "today", "02:00" and a timestamp in an email mean. A request picks
92
+ * its own with `?tz=Europe/Paris` or an X-Timezone header (narrowed to
93
+ * `supported` when you list it), and handlers read it as `req.timezone`
94
+ * with helpers on `req.time`; code with no request uses `ctx.time`.
95
+ *
96
+ * `default` is also the zone of cron patterns given to `ctx.queues`, and
97
+ * — unless `applyToProcess: false` — the process zone, so log times,
98
+ * daily log rotation and `Date#getHours()` agree with it.
99
+ */
100
+ timezone: {
101
+ default: "UTC",
102
+ },
103
+
53
104
  /**
54
105
  * List-endpoint defaults, so every module paginates identically and no
55
106
  * caller can ask for ten thousand rows. → modules/users/routes
@@ -67,7 +118,9 @@ export default appConfig;
67
118
  * number, not `unknown`. `pnpm sync` does this automatically for
68
119
  * module.config.ts factories; the app-level seed types itself here.
69
120
  * Partial because the config object is empty until this factory runs.
121
+ * `server`, `mongoose` and `bullmq` are left out: core and the providers
122
+ * already type the keys they read.
70
123
  */
71
124
  declare module "@tulipes/core/config" {
72
- interface GlobalConfig extends Partial<ReturnType<typeof appConfig>> {}
125
+ interface GlobalConfig extends Partial<Omit<ReturnType<typeof appConfig>, "server" | "mongoose" | "bullmq">> {}
73
126
  }