@tulipes/cli 0.2.0 → 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.
- package/dist/config-check.js +1 -1
- package/dist/env-check.d.ts +5 -3
- package/dist/env-check.js +26 -19
- package/dist/env-check.js.map +1 -1
- package/dist/main.js +83 -19
- package/dist/main.js.map +1 -1
- package/dist/migrate-0.12-env.d.ts +36 -0
- package/dist/migrate-0.12-env.js +250 -0
- package/dist/migrate-0.12-env.js.map +1 -0
- package/dist/migrate-0.12-scaffold.d.ts +1 -0
- package/dist/migrate-0.12-scaffold.js +197 -0
- package/dist/migrate-0.12-scaffold.js.map +1 -0
- package/dist/migrate-0.12.d.ts +47 -0
- package/dist/migrate-0.12.js +210 -0
- package/dist/migrate-0.12.js.map +1 -0
- package/dist/migrate.d.ts +3 -1
- package/dist/migrate.js +11 -5
- package/dist/migrate.js.map +1 -1
- package/dist/new-module.d.ts +7 -0
- package/dist/new-module.js +35 -16
- package/dist/new-module.js.map +1 -1
- package/dist/roles.d.ts +15 -0
- package/dist/roles.js +54 -0
- package/dist/roles.js.map +1 -0
- package/dist/routes.d.ts +7 -2
- package/dist/routes.js +19 -10
- package/dist/routes.js.map +1 -1
- package/dist/spec.d.ts +2 -0
- package/dist/spec.js +13 -11
- package/dist/spec.js.map +1 -1
- package/dist/sync.js +66 -53
- package/dist/sync.js.map +1 -1
- package/package.json +4 -4
- package/templates/AGENTS.md +35 -23
- package/templates/app/.envs/.env.development +2 -1
- package/templates/app/.envs/.env.production +2 -2
- package/templates/app/.envs/.env.staging +2 -2
- package/templates/app/README.md +44 -29
- package/templates/app/config/app.config.ts +60 -7
- package/templates/app/docs/agent-manifest.json +8 -681
- package/templates/app/modules/agent/controllers/{agent.controllers.ts → web.agent.controllers.ts} +1 -1
- package/templates/app/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +4 -4
- package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +20 -12
- package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +4 -2
- package/templates/app/modules/auth/controllers/{auth.controllers.ts → web.auth.controllers.ts} +1 -1
- package/templates/app/modules/auth/helpers/passport.ts +1 -1
- package/templates/app/modules/auth/helpers/session-cache.ts +3 -3
- package/templates/app/modules/auth/helpers/tokens.ts +3 -5
- package/templates/app/modules/auth/module.acl.ts +31 -9
- package/templates/app/modules/auth/module.config.ts +5 -3
- package/templates/app/modules/auth/package.json +10 -10
- package/templates/app/modules/auth/routes/{auth.routes.ts → web.auth.routes.ts} +15 -10
- package/templates/app/modules/auth/tests/auth.test.ts +1 -1
- package/templates/app/modules/config/controllers/{config.controllers.ts → bo.config.controllers.ts} +3 -2
- package/templates/app/modules/config/helpers/audit.ts +1 -1
- package/templates/app/modules/config/helpers/describe.ts +35 -26
- package/templates/app/modules/config/module.acl.ts +1 -1
- package/templates/app/modules/config/routes/{config.routes.ts → bo.config.routes.ts} +5 -5
- package/templates/app/modules/config/tests/config.test.ts +14 -6
- package/templates/app/modules/core/helpers/cache/index.ts +8 -7
- package/templates/app/modules/core/helpers/lazy.ts +3 -3
- package/templates/app/modules/core/helpers/logger/index.ts +24 -16
- package/templates/app/modules/core/module.config.ts +5 -3
- package/templates/app/modules/core/package.json +46 -19
- package/templates/app/modules/core/routes/core.routes.ts +4 -4
- package/templates/app/modules/hello/agent/hello.agent.ts +2 -2
- package/templates/app/modules/hello/controllers/{hello.controllers.ts → web/hello.controllers.ts} +1 -1
- package/templates/app/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +2 -2
- package/templates/app/modules/hello/module.acl.ts +12 -7
- package/templates/app/modules/hello/queues/hello.queues.ts +1 -1
- package/templates/app/modules/hello/routes/{greetings.routes.ts → web/hello.greetings.routes.ts} +4 -4
- package/templates/app/modules/hello/routes/{hello.routes.ts → web/hello.routes.ts} +14 -8
- package/templates/app/modules/security/helpers/rate-limit.ts +3 -2
- package/templates/app/modules/security/routes/security.routes.ts +4 -4
- package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +2 -2
- package/templates/app/modules/settings/controllers/{settings.controllers.ts → bo.settings.controllers.ts} +7 -6
- package/templates/app/modules/settings/helpers/log-stream.ts +6 -5
- package/templates/app/modules/settings/routes/{settings.routes.ts → bo.settings.routes.ts} +3 -3
- package/templates/app/modules/settings/tests/settings.test.ts +21 -25
- package/templates/app/modules/users/controllers/{users.controllers.ts → bo.users.controllers.ts} +5 -55
- package/templates/{overlays/no-queues/modules/users/controllers/users.controllers.ts → app/modules/users/controllers/web.users.controllers.ts} +4 -42
- package/templates/app/modules/users/module.acl.ts +4 -4
- package/templates/app/modules/users/queues/users.queues.ts +1 -1
- package/templates/app/modules/users/routes/bo.users.routes.ts +39 -0
- package/templates/app/modules/users/routes/web.users.routes.ts +45 -0
- package/templates/app/nginx/production.conf +1 -1
- package/templates/app/scripts/deploy-nginx.ts +3 -2
- package/templates/app/tests/support.ts +4 -3
- package/templates/claude/skills/tulipes-agent-tool/SKILL.md +6 -4
- package/templates/claude/skills/tulipes-boot-errors/SKILL.md +13 -7
- package/templates/claude/skills/tulipes-config/SKILL.md +6 -4
- package/templates/claude/skills/tulipes-endpoint/SKILL.md +34 -15
- package/templates/claude/skills/tulipes-env-variable/SKILL.md +62 -37
- package/templates/claude/skills/tulipes-i18n/SKILL.md +20 -1
- package/templates/claude/skills/tulipes-module/SKILL.md +10 -5
- package/templates/claude/skills/tulipes-permissions/SKILL.md +123 -35
- package/templates/claude/skills/tulipes-queue/SKILL.md +3 -2
- package/templates/claude/skills/tulipes-settings/SKILL.md +9 -8
- package/templates/claude/skills/tulipes-spec/SKILL.md +12 -2
- package/templates/minimal/config/app.config.ts +13 -3
- package/templates/minimal/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +2 -2
- package/templates/minimal/modules/core/module.acl.ts +7 -1
- package/templates/minimal/modules/core/package.json +4 -4
- package/templates/minimal/modules/hello/module.acl.ts +1 -1
- package/templates/minimal/modules/hello/routes/{health.routes.ts → web.hello.health.routes.ts} +1 -1
- package/templates/minimal/modules/hello/routes/{hello.routes.ts → web.hello.routes.ts} +1 -1
- package/templates/overlays/no-queues/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +1 -1
- package/templates/overlays/no-queues/modules/users/controllers/bo.users.controllers.ts +57 -0
- package/templates/versions.json +8 -8
- package/templates/app/modules/users/routes/users.routes.ts +0 -60
package/dist/spec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../src/spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../src/spec.ts"],"names":[],"mappings":"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"}
|
package/dist/sync.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { BootReport } from "@tulipes/core/errors";
|
|
4
|
-
import {
|
|
5
|
-
import { discoverModules } from "@tulipes/core/modules";
|
|
6
|
-
import { sortModules } from "@tulipes/core/modules";
|
|
7
|
-
import { exploreModules } from "@tulipes/core/modules";
|
|
4
|
+
import { discoverModules, exploreModules, sortModules } from "@tulipes/core/modules";
|
|
8
5
|
/**
|
|
9
6
|
* `tulipes sync` — everything derivable from the declarations, derived:
|
|
10
7
|
*
|
|
11
|
-
* types/tulipes.d.ts
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
8
|
+
* types/tulipes.d.ts EnvValues, GlobalConfig, KnownModels and KnownJobs
|
|
9
|
+
* augmentations. A config namespace is typed as the
|
|
10
|
+
* OUTPUT of the schema its module.config.ts declares,
|
|
11
|
+
* intersected with the read-only environment values
|
|
12
|
+
* its package.json declares; a model as its file's
|
|
13
|
+
* default export and a queue's job data as its file's
|
|
14
|
+
* `jobs` schema, all via `typeof import(...)` — the
|
|
15
|
+
* compiler does the inference, codegen never executes
|
|
16
|
+
* module code. The one thing read from source is each
|
|
17
|
+
* model's name, which keys the interface and so must
|
|
18
|
+
* be a literal.
|
|
20
19
|
* .env.example every variable, grouped by module and `group`.
|
|
21
20
|
*
|
|
22
21
|
* Both files are committed and marked generated.
|
|
@@ -28,18 +27,14 @@ export const TYPES_FILE = "tulipes.d.ts";
|
|
|
28
27
|
export async function runSync(rootDir) {
|
|
29
28
|
const report = new BootReport();
|
|
30
29
|
const modulesDir = join(rootDir, "modules");
|
|
31
|
-
|
|
30
|
+
// The manifests carry the environment declarations; load order keeps
|
|
31
|
+
// the output stable.
|
|
32
32
|
const order = sortModules(discoverModules(modulesDir, report), report);
|
|
33
33
|
report.throwIfAny("sync");
|
|
34
|
-
const metaByModule = new Map(metas.map((meta) => [meta.module, meta]));
|
|
35
|
-
// Meta discovery walks folders; align it to load order for stable output.
|
|
36
|
-
const orderedMetas = order
|
|
37
|
-
.map((module) => metaByModule.get(module.name))
|
|
38
|
-
.filter((meta) => meta !== undefined);
|
|
39
34
|
const typesDir = join(rootDir, "types");
|
|
40
35
|
mkdirSync(typesDir, { recursive: true });
|
|
41
36
|
const dtsPath = join(typesDir, TYPES_FILE);
|
|
42
|
-
writeFileSync(dtsPath, renderDts(rootDir,
|
|
37
|
+
writeFileSync(dtsPath, renderDts(rootDir, order));
|
|
43
38
|
console.log(`wrote ${relative(rootDir, dtsPath)}`);
|
|
44
39
|
const legacyPath = join(typesDir, LEGACY_DTS);
|
|
45
40
|
if (existsSync(legacyPath) && readFileSync(legacyPath, "utf8").startsWith(GENERATED_MARKER)) {
|
|
@@ -47,29 +42,39 @@ export async function runSync(rootDir) {
|
|
|
47
42
|
console.log(`removed ${relative(rootDir, legacyPath)} (now ${TYPES_FILE})`);
|
|
48
43
|
}
|
|
49
44
|
const envExamplePath = join(rootDir, ".env.example");
|
|
50
|
-
writeFileSync(envExamplePath, renderEnvExample(
|
|
45
|
+
writeFileSync(envExamplePath, renderEnvExample(order));
|
|
51
46
|
console.log(`wrote ${relative(rootDir, envExamplePath)}`);
|
|
52
47
|
}
|
|
53
|
-
function renderDts(rootDir,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
function renderDts(rootDir, order) {
|
|
49
|
+
// `env.<module>.<name>` in config/app.config.ts: one block per module that
|
|
50
|
+
// declares anything, each value read-only, optional when nothing
|
|
51
|
+
// guarantees it (no default, not required).
|
|
52
|
+
const envLines = order
|
|
53
|
+
.filter((module) => Object.keys(module.env).length > 0)
|
|
54
|
+
.flatMap((module) => [
|
|
55
|
+
` ${JSON.stringify(module.name)}: {`,
|
|
56
|
+
...envFieldLines(module, " "),
|
|
57
|
+
` };`,
|
|
58
|
+
]);
|
|
58
59
|
const explored = exploreModules(order);
|
|
59
60
|
// `.ts` specifiers: the app imports its own files that way (Node strips
|
|
60
61
|
// the types natively) and tsconfig allows importing them.
|
|
61
62
|
const specifierOf = (file) => "./" + relative(join(rootDir, "types"), file);
|
|
62
|
-
//
|
|
63
|
-
// back to GlobalConfig's index
|
|
63
|
+
// A module's namespace is its settings' output, its environment values,
|
|
64
|
+
// or both; a module with neither falls back to GlobalConfig's index
|
|
65
|
+
// signature. Optional on purpose: a namespace only exists once phase 6
|
|
66
|
+
// has loaded it, and an empty config seed must stay assignable.
|
|
64
67
|
const configLines = explored
|
|
65
|
-
.filter((files) => files.configFile)
|
|
66
|
-
.flatMap((files) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
.filter((files) => files.configFile || Object.keys(files.module.env).length > 0)
|
|
69
|
+
.flatMap((files) => {
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (files.configFile) {
|
|
72
|
+
parts.push(`import("zod/v4").output<(typeof import("${specifierOf(files.configFile)}"))["default"]["schema"]>`);
|
|
73
|
+
}
|
|
74
|
+
if (Object.keys(files.module.env).length > 0)
|
|
75
|
+
parts.push(`EnvValues[${JSON.stringify(files.module.name)}]`);
|
|
76
|
+
return [` ${JSON.stringify(files.module.name)}?: ${parts.join(" & ")};`];
|
|
77
|
+
});
|
|
73
78
|
// A model's name keys the interface, so it is read from the file — the
|
|
74
79
|
// one place codegen looks at source rather than types. A name that is not
|
|
75
80
|
// a literal is reported and the model stays reachable through the
|
|
@@ -109,13 +114,10 @@ function renderDts(rootDir, metas, order) {
|
|
|
109
114
|
return [
|
|
110
115
|
`${GENERATED_MARKER} — do not edit.`,
|
|
111
116
|
"",
|
|
112
|
-
'declare module "@tulipes/core/env" {',
|
|
113
|
-
" interface KnownVariables {",
|
|
114
|
-
...variableLines,
|
|
115
|
-
" }",
|
|
116
|
-
"}",
|
|
117
|
-
"",
|
|
118
117
|
'declare module "@tulipes/core/config" {',
|
|
118
|
+
" interface EnvValues {",
|
|
119
|
+
...envLines,
|
|
120
|
+
" }",
|
|
119
121
|
" interface GlobalConfig {",
|
|
120
122
|
...configLines,
|
|
121
123
|
" }",
|
|
@@ -135,6 +137,16 @@ export function modelNameOf(file) {
|
|
|
135
137
|
const scope = DEFAULT_EXPORT_OBJECT.exec(source)?.[1] ?? source;
|
|
136
138
|
return NAME_LITERAL.exec(scope)?.[2] ?? NAME_LITERAL.exec(source)?.[2];
|
|
137
139
|
}
|
|
140
|
+
/** `/** doc *\/ readonly name: type;` for each of a module's declared values. */
|
|
141
|
+
function envFieldLines(module, indent) {
|
|
142
|
+
return Object.entries(module.env).flatMap(([name, spec]) => {
|
|
143
|
+
const guaranteed = spec.required === true || spec.default !== undefined;
|
|
144
|
+
return [
|
|
145
|
+
`${indent}/** ${spec.key} — ${spec.description} */`,
|
|
146
|
+
`${indent}readonly ${name}: ${tsTypeOf(spec)}${guaranteed ? "" : " | undefined"};`,
|
|
147
|
+
];
|
|
148
|
+
});
|
|
149
|
+
}
|
|
138
150
|
function tsTypeOf(spec) {
|
|
139
151
|
switch (spec.type) {
|
|
140
152
|
case "number":
|
|
@@ -147,29 +159,30 @@ function tsTypeOf(spec) {
|
|
|
147
159
|
return "string";
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
|
-
function renderEnvExample(
|
|
162
|
+
function renderEnvExample(order) {
|
|
151
163
|
const lines = [
|
|
152
164
|
"# AUTO-GENERATED by `tulipes sync` from every module's package.json tulipes.env.",
|
|
153
165
|
"# Copy into .envs/.env.<mode> and fill in the blanks.",
|
|
154
166
|
];
|
|
155
|
-
for (const
|
|
156
|
-
|
|
167
|
+
for (const module of order) {
|
|
168
|
+
const variables = Object.entries(module.env);
|
|
169
|
+
if (variables.length === 0)
|
|
157
170
|
continue;
|
|
158
|
-
lines.push("", `# ━━━ ${
|
|
159
|
-
// Stable grouping: order of first appearance, ungrouped
|
|
171
|
+
lines.push("", `# ━━━ ${module.name} ━━━`);
|
|
172
|
+
// Stable grouping: order of first appearance, ungrouped variables last.
|
|
160
173
|
const groups = new Map();
|
|
161
|
-
for (const
|
|
162
|
-
const
|
|
163
|
-
(groups.get(
|
|
174
|
+
for (const entry of variables) {
|
|
175
|
+
const group = entry[1].group ?? "";
|
|
176
|
+
(groups.get(group) ?? groups.set(group, []).get(group)).push(entry);
|
|
164
177
|
}
|
|
165
178
|
for (const [group, specs] of [...groups.entries()].sort(([a], [b]) => Number(a === "") - Number(b === ""))) {
|
|
166
179
|
if (group)
|
|
167
180
|
lines.push("", `# ── ${group} ──`);
|
|
168
|
-
for (const spec of specs) {
|
|
181
|
+
for (const [name, spec] of specs) {
|
|
169
182
|
const constraint = spec.type === "enum" ? `enum: ${spec.enum.join(" | ")}` : spec.type;
|
|
170
183
|
const requirement = spec.required ? ", REQUIRED" : "";
|
|
171
|
-
lines.push(`# ${spec.description} (${constraint}${requirement})`);
|
|
172
|
-
lines.push(`${spec.
|
|
184
|
+
lines.push(`# ${spec.description} (${constraint}${requirement}) → config.${module.name}.${name}`);
|
|
185
|
+
lines.push(`${spec.key}=${spec.default ?? ""}`);
|
|
173
186
|
}
|
|
174
187
|
}
|
|
175
188
|
}
|
package/dist/sync.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tulipes/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-rc.2",
|
|
4
4
|
"description": "The tulipes command line: scaffold, sync, inspect, describe and run Tulipes apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@tulipes/core": "^0.
|
|
22
|
+
"@tulipes/core": "^0.12.0-rc.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.2.0",
|
|
26
26
|
"typescript": "^7.0.2",
|
|
27
|
-
"@tulipes/spec": "0.
|
|
27
|
+
"@tulipes/spec": "0.4.0-rc.2"
|
|
28
28
|
},
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"url": "https://github.com/kemora13conf/bp-backend-express/issues"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@tulipes/spec": "^0.
|
|
49
|
+
"@tulipes/spec": "^0.4.0-rc.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|
|
52
52
|
"@tulipes/spec": {
|
package/templates/AGENTS.md
CHANGED
|
@@ -27,10 +27,12 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
27
27
|
## Non-negotiable rules
|
|
28
28
|
|
|
29
29
|
1. **Never read `process.env` directly, and keep the environment small.**
|
|
30
|
-
A value is an environment
|
|
30
|
+
A value is an environment value only if boot needs it before the
|
|
31
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
|
|
33
|
-
`
|
|
32
|
+
is a secret: declared under `tulipes.env` in the module's `package.json`
|
|
33
|
+
as `"name": { "key": "VARIABLE", … }`, read as
|
|
34
|
+
`ctx.config.<module>.<name>` — read-only, invisible to listings, beside
|
|
35
|
+
the module's settings. Everything else is a *setting*: declared
|
|
34
36
|
with a zod schema in that module's `module.config.ts`, stored in the
|
|
35
37
|
settings store (the `settings` collection here; a JSON file under
|
|
36
38
|
`config/` in an app without the Mongo store), edited through the
|
|
@@ -52,9 +54,10 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
52
54
|
5. **Run `pnpm sync` after touching a module's `tulipes.env`, a
|
|
53
55
|
settings schema in `module.config.ts`, a model file or a queue file's
|
|
54
56
|
`jobs` export.** It regenerates `types/tulipes.d.ts` and `.env.example`;
|
|
55
|
-
stale generated types are a lie. With it, `ctx.
|
|
56
|
-
typed from the model's schema and
|
|
57
|
-
the queue's job schema, so a wrong
|
|
57
|
+
stale generated types are a lie. With it, `ctx.config.core.port` is a
|
|
58
|
+
`number`, `ctx.models.get("User")` is typed from the model's schema and
|
|
59
|
+
`queues.add("users.welcome", …)` from the queue's job schema, so a wrong
|
|
60
|
+
name or payload does not compile. `module.config.ts`
|
|
58
61
|
has exactly one export, `defineConfig({ schema })`; initialization and
|
|
59
62
|
cleanup belong to `bootstrap/*.bootstrap.ts`, which may return the
|
|
60
63
|
function that closes what it opened, and long-lived work ends on
|
|
@@ -64,11 +67,14 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
64
67
|
`throw new HttpError(404, "notFound")`, where the message is an i18n
|
|
65
68
|
key and the code derives from it. Never hand-roll an error shape.
|
|
66
69
|
→ skill `tulipes-response`
|
|
67
|
-
7. **Every route declares a RAI**: put `rai({ id: "
|
|
68
|
-
each endpoint handler chain, including routes on a nested router.
|
|
69
|
-
The id is
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
7. **Every route declares a RAI**: put `rai({ id: "users:bo:list" })` first
|
|
71
|
+
in each endpoint handler chain, including routes on a nested router.
|
|
72
|
+
The id is always `module:scope:resource[:extra]` — `users:mobile:profile`,
|
|
73
|
+
`users:bo:detail:delete` — where the scope is the surface it serves
|
|
74
|
+
(`web`, `bo`, …). It is also the ACL permission, and core refuses to boot
|
|
75
|
+
if a route is missing one or the id has another shape. Public is
|
|
76
|
+
`acl.allow("guest", "users:web:list")`, never a missing check.
|
|
77
|
+
→ skills `tulipes-endpoint`, `tulipes-permissions`
|
|
72
78
|
8. **Validate input with a zod schema on the RAI** (`body`, `query`,
|
|
73
79
|
`params`, from `zod/v4`) rather than hand-rolled `if` checks. The same
|
|
74
80
|
declaration generates the OpenAPI and Postman documents, so they cannot
|
|
@@ -85,7 +91,7 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
85
91
|
the auth module's `Credential` model, which is `select: false`.
|
|
86
92
|
12. **When auth is installed, a new feature module's namespace goes in BOTH the `admin` and
|
|
87
93
|
`sysadmin` grants** in `modules/auth/module.acl.ts`. Neither role holds
|
|
88
|
-
`*` — a wildcard would reach `config:write` and `settings:*` and make
|
|
94
|
+
`*` — a wildcard would reach `config:bo:write` and `settings:*` and make
|
|
89
95
|
the sysadmin boundary a label rather than a rule. → skills
|
|
90
96
|
`tulipes-config`, `tulipes-settings`
|
|
91
97
|
13. **Cross-module imports use the package name** (`@app/users`,
|
|
@@ -106,19 +112,23 @@ require dedicated TEST_MONGO_URI and TEST_REDIS_URL services.
|
|
|
106
112
|
never reach into another module's files from a test.
|
|
107
113
|
|
|
108
114
|
For endpoint changes, read `tulipes-endpoint`: native routers live in
|
|
109
|
-
`routes
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
`routes/`, named scope-first — `routes/web.users.routes.ts`,
|
|
116
|
+
`routes/bo.users.routes.ts`, and a folder per scope when one scope has
|
|
117
|
+
several files (`routes/web/hello.greetings.routes.ts`); controllers mirror
|
|
118
|
+
them (`controllers/bo.users.controllers.ts` exports `BoUsersController`).
|
|
119
|
+
They are plain `(ctx) => Router` functions: build the router from
|
|
120
|
+
the context, return it for the configured prefix, and read models and
|
|
121
|
+
config inside handlers, never while building — `tulipes routes` and
|
|
122
|
+
`tulipes spec` build every router with nothing acquired, and an environment
|
|
123
|
+
value read while building throws. Global middleware that needs one is built
|
|
124
|
+
on its first request with `lazy()` from `@app/core`.
|
|
115
125
|
|
|
116
126
|
## Where things go
|
|
117
127
|
|
|
118
128
|
| Decision | Location |
|
|
119
129
|
|---|---|
|
|
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` |
|
|
130
|
+
| Connection string, secret, or anything boot needs before the database answers | `tulipes.env` in that module's `package.json`, read at `ctx.config.<module>.<name>` |
|
|
131
|
+
| Policy shared by ≥2 modules and the same everywhere (pagination, locales); wiring the environment to what core and the providers read (`server.port`, `mongoose.*`, `bullmq.redisUrl`) | `config/app.config.ts` |
|
|
122
132
|
| 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
133
|
| Opening a client, seeding rows, ensuring indexes, and closing them again | that module's `bootstrap/*.bootstrap.ts`, returning the cleanup |
|
|
124
134
|
| A string a user will read | that module's `i18n/<locale>.json`, reached with `req.t` |
|
|
@@ -128,7 +138,7 @@ middleware that needs the environment is built on its first request with
|
|
|
128
138
|
| Helper used by ≥2 modules | the sys core module, exported from its `lib/index.ts` and imported as `@app/core` |
|
|
129
139
|
| Helper used by one module | that module's `helpers/` |
|
|
130
140
|
| One-off task (backfill, export, admin chore) | `scripts/`, booted with `mode: "script"` |
|
|
131
|
-
| Anything you would `console.log` | `moduleLogger(ctx
|
|
141
|
+
| Anything you would `console.log` | `moduleLogger(ctx, "<module>")` from `@app/core` |
|
|
132
142
|
|
|
133
143
|
## Boot order (why load order matters)
|
|
134
144
|
|
|
@@ -145,11 +155,13 @@ luck.
|
|
|
145
155
|
| `pnpm dev` | run the backend process (regenerates types first) |
|
|
146
156
|
| `pnpm worker` | run the queue-consumer process |
|
|
147
157
|
| `pnpm sync` | regenerate `types/tulipes.d.ts` + `.env.example` |
|
|
158
|
+
| `pnpm tulipes routes [--scope s]` | every endpoint with the roles that reach it, offline |
|
|
159
|
+
| `pnpm tulipes roles [--role r]` | every role with its description, extras and grants, offline |
|
|
148
160
|
| `pnpm tulipes agent manifest [--role r]` | print every tool the app exposes to agents, offline |
|
|
149
161
|
| `pnpm test` | every `tests/` and `modules/*/tests/` file; each boots the app once through `@tulipes/testing` |
|
|
150
|
-
| `pnpm check` |
|
|
162
|
+
| `pnpm check` | resolve every environment value and show where it lands, without booting |
|
|
151
163
|
| `pnpm typecheck` | typecheck the whole app |
|
|
152
|
-
| `pnpm tulipes new module <name
|
|
164
|
+
| `pnpm tulipes new module <name> [--scope s]` | scaffold a module (its first endpoint in scope `s`, default `web`) |
|
|
153
165
|
| `tulipes update` | upgrade the global cli and the framework in every manifest |
|
|
154
166
|
| `pnpm script scripts/x.ts` | run a one-off script against the real app |
|
|
155
167
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Development. NOT committed — .gitignore excludes .envs/, because a
|
|
2
2
|
# file holding working credentials does not belong in a repository.
|
|
3
3
|
# The committed contract is .env.example at the root: copy it here and
|
|
4
|
-
# fill it in.
|
|
4
|
+
# fill it in. Each variable lands on one module's config — `pnpm check`
|
|
5
|
+
# shows where. Real deployments inject variables through the container
|
|
5
6
|
# environment, which always wins over this file.
|
|
6
7
|
|
|
7
8
|
PORT=3000
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# The blanks below are intentional. Real deployments inject MONGO_URI,
|
|
6
6
|
# REDIS_URL and any secret through the container environment, which
|
|
7
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.
|
|
8
|
+
# report naming every missing variable — run `pnpm check` to see it,
|
|
9
|
+
# and where each value lands, without starting the app.
|
|
10
10
|
|
|
11
11
|
PORT=3000
|
|
12
12
|
LOG_LEVEL=info
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# The blanks below are intentional. Real deployments inject MONGO_URI,
|
|
6
6
|
# REDIS_URL and any secret through the container environment, which
|
|
7
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.
|
|
8
|
+
# report naming every missing variable — run `pnpm check` to see it,
|
|
9
|
+
# and where each value lands, without starting the app.
|
|
10
10
|
|
|
11
11
|
PORT=3000
|
|
12
12
|
LOG_LEVEL=info
|
package/templates/app/README.md
CHANGED
|
@@ -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`
|
|
14
|
-
|
|
15
|
-
|
|
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 #
|
|
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
|
|
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
|
|
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.
|
|
83
|
-
|
|
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
|
|
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,
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
|
177
|
-
settings store answers: `LOG_LEVEL`, `LOG_CONSOLE`, `LOG_FILE` and
|
|
178
|
-
`LOG_DIR`, declared in `modules/core/package.json
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
fail on configuration before it fails
|
|
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
|