create-warlock 5.30.0 → 5.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +29 -2
  2. package/README.md +66 -15
  3. package/esm/commands/create-new-app/index.mjs +19 -16
  4. package/esm/commands/create-new-app/index.mjs.map +1 -1
  5. package/esm/commands/create-new-app/types.d.mts +2 -1
  6. package/esm/commands/create-warlock-app/index.mjs +1 -0
  7. package/esm/commands/create-warlock-app/index.mjs.map +1 -1
  8. package/esm/features/agent-kit-targets.mjs.map +1 -1
  9. package/esm/features/database-drivers.mjs.map +1 -1
  10. package/esm/features/features-map.mjs +12 -12
  11. package/esm/features/features-map.mjs.map +1 -1
  12. package/esm/flags/assert-package-manager.mjs.map +1 -1
  13. package/esm/flags/definitions.mjs +5 -0
  14. package/esm/flags/definitions.mjs.map +1 -1
  15. package/esm/flags/seed-from-flags.mjs +1 -0
  16. package/esm/flags/seed-from-flags.mjs.map +1 -1
  17. package/esm/helpers/app.mjs +44 -0
  18. package/esm/helpers/app.mjs.map +1 -1
  19. package/esm/helpers/exec.mjs.map +1 -1
  20. package/esm/helpers/insert-import.mjs.map +1 -1
  21. package/esm/helpers/package-manager.mjs.map +1 -1
  22. package/esm/helpers/pnpm-build-approvals.mjs.map +1 -1
  23. package/esm/helpers/project-builder-helpers.mjs.map +1 -1
  24. package/esm/helpers/warlock-versions.mjs.map +1 -1
  25. package/esm/index.mjs +5 -1
  26. package/esm/index.mjs.map +1 -1
  27. package/esm/prompts/ask-agent-targets.mjs.map +1 -1
  28. package/esm/prompts/ask-ai-features.mjs.map +1 -1
  29. package/esm/prompts/ask-features.mjs +53 -0
  30. package/esm/prompts/ask-features.mjs.map +1 -0
  31. package/esm/prompts/ask-lint-toolchain.mjs +21 -0
  32. package/esm/prompts/ask-lint-toolchain.mjs.map +1 -0
  33. package/esm/prompts/fit-to-terminal.mjs.map +1 -1
  34. package/esm/summary.mjs +11 -1
  35. package/esm/summary.mjs.map +1 -1
  36. package/esm/ui/banner.mjs +114 -46
  37. package/esm/ui/banner.mjs.map +1 -1
  38. package/esm/ui/report.mjs.map +1 -1
  39. package/esm/ui/spinners.mjs.map +1 -1
  40. package/esm/ui/wrap-text.mjs +55 -0
  41. package/esm/ui/wrap-text.mjs.map +1 -0
  42. package/llms-full.txt +71 -25
  43. package/package.json +3 -3
  44. package/skills/create-a-warlock-project/SKILL.md +71 -25
  45. package/templates/toolchains/oxc/.oxfmtrc.json +12 -0
  46. package/templates/toolchains/oxc/.oxlintrc.json +24 -0
  47. package/templates/toolchains/oxc/.vscode/extensions.json +3 -0
  48. package/templates/toolchains/oxc/.vscode/settings.json +35 -0
  49. package/templates/warlock/AGENTS.md +22 -2
  50. package/templates/warlock/README.md +5 -3
  51. package/templates/warlock/package.json +6 -4
  52. package/templates/warlock/skills/api-design/SKILL.md +2 -2
  53. package/templates/warlock/skills/code-standards/SKILL.md +18 -4
  54. package/templates/warlock/skills/security-baseline/SKILL.md +12 -1
  55. package/templates/warlock/skills/testing-strategy/SKILL.md +17 -0
  56. package/templates/warlock/src/app/auth/routes.ts +25 -5
  57. package/templates/warlock/src/app/auth/utils/types.ts +6 -1
  58. package/templates/warlock/src/app/contact/routes.ts +1 -1
  59. package/templates/warlock/src/app/locale/routes.ts +1 -1
  60. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +10 -2
  61. package/templates/warlock/src/app/users/schema/list-users.schema.ts +15 -0
  62. package/templates/warlock/src/config/tests.ts +8 -0
  63. package/templates/warlock/src/web/404.css +2 -6
@@ -32,7 +32,7 @@ The first positional argument is the project name; it also becomes the directory
32
32
 
33
33
  1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given), API-only or full-stack web (`--stack`), then a database unless `--db` or `--no-db` already supplied it. The stack is the only choice that changes the generated app's shape.
34
34
  2. **`--yes` (or no TTY at all — CI, a script, an agent harness)** — fully headless. Every answer comes from a flag or its default; nothing is ever prompted, and stdin is never read.
35
- 3. **`--interactive` (alias `--customize`)** — the full long-form wizard: project name, package manager, database driver, features, AI providers, git, JWT — one prompt after another, exactly like earlier `create-warlock` releases.
35
+ 3. **`--interactive` (alias `--customize`)** — the full long-form wizard: project name, package manager, database driver, features, AI providers, lint/format toolchain, git, JWT — one prompt after another, exactly like earlier `create-warlock` releases.
36
36
 
37
37
  **Without a terminal, `--yes` is not required.** When stdin is not a TTY — CI, a
38
38
  script, a container, an agent harness — the scaffolder does not try to prompt,
@@ -46,7 +46,7 @@ that is just the project name, the one required answer with no default:
46
46
  ```
47
47
  A project name is required and no terminal is available to ask for one. Pass it as
48
48
  the first argument or --name=<name>. Non-interactive flags: --yes, --pm=<npm|yarn|pnpm>,
49
- --db=<driver>|--no-db, --features=<list>, --ai=<list>, --git|--no-git, --jwt|--no-jwt.
49
+ --db=<driver>|--no-db, --features=<list>, --ai=<list>, --lint=<oxc|eslint>, --git|--no-git, --jwt|--no-jwt.
50
50
  ```
51
51
 
52
52
  ## After scaffolding: the decisions summary
@@ -55,26 +55,30 @@ Because almost every choice defaults silently, `create-warlock` prints what it
55
55
  decided right after scaffolding, e.g.:
56
56
 
57
57
  ```
58
- Using npm · database: none · agent-kit targets: claude
58
+ Using npm · database: none · lint: eslint · agent-kit targets: claude
59
+ Features: none
59
60
  change these — package manager: --pm, database: --db/--no-db, agents: --agents,
60
- features: --features, ai: --ai, git: --git/--no-git, jwt: --jwt/--no-jwt — or
61
+ features: --features, ai: --ai, lint: --lint, git: --git/--no-git, jwt: --jwt/--no-jwt — or
61
62
  re-run with --interactive for the full wizard.
62
63
  ```
63
64
 
64
65
  ## Flags
65
66
 
66
- | Flag | Takes value | Default | Purpose |
67
- | --------------------------- | ----------- | -------------- | ------------------------------------------------------------------- |
68
- | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
69
- | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
70
- | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
71
- | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
72
- | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
73
- | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
74
- | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
75
- | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
76
- | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit).|
77
- | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
67
+ | Flag | Takes value | Default | Purpose |
68
+ | ------------------------------ | ----------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
69
+ | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
70
+ | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
71
+ | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
72
+ | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
73
+ | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
74
+ | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
75
+ | `--lint` | yes | `eslint` | `oxc` (Oxlint/Oxfmt) or `eslint` (ESLint/Prettier); unknown or missing values fail before writes. |
76
+ | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
77
+ | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
78
+ | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit). |
79
+ | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
80
+ | `--yes` / `-y` | no | off | Fully headless mode. |
81
+ | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
78
82
 
79
83
  With `--pm=pnpm`, the scaffold reads `pnpm --version` and records install-script
80
84
  approvals (esbuild, and Sharp when the `image` feature is selected) where that
@@ -85,10 +89,8 @@ no workspace file (those versions reject one without `packages:`) but
85
89
  retained. If selected feature dependencies do
86
90
  not install, JWT generation is skipped and reported rather than retried through
87
91
  the package manager.
88
- | `--yes` / `-y` | no | off | Fully headless mode. |
89
- | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
90
92
 
91
- Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--features` / `--ai` / `--agents` keys fail fast before any install.
93
+ Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--pm` / `--db` / `--features` / `--ai` / `--agents` / `--lint` values fail fast before any write or install.
92
94
 
93
95
  `--help`/`-h` and `--version`/`-v` short-circuit before anything else — no prompt, filesystem write, or network call, and `--help` wins even over a positional project name. `--version` prints the installed `create-warlock` version. `--help` lists every flag above together with its default.
94
96
 
@@ -102,18 +104,18 @@ fallback that always works — even fully offline. Any other target
102
104
  `goose`, …) is validated against the list published at
103
105
  https://mongez.js.org/agent-kit/llms.txt, fetched once and cached locally. If
104
106
  that fetch fails for an explicitly requested non-default target, the run fails
105
- visibly and names every target it *could* determine — it never silently
107
+ visibly and names every target it _could_ determine — it never silently
106
108
  guesses.
107
109
 
108
110
  ## Valid keys
109
111
 
110
112
  **Database drivers** (`--db`):
111
113
 
112
- | Key | Default port | Status |
113
- | ---------- | ------------ | ----------- |
114
- | `mongodb` | 27017 | available |
115
- | `postgres` | 5432 | available |
116
- | `mysql` | 3306 | coming soon (disabled in the wizard) |
114
+ | Key | Default port | Status |
115
+ | ---------- | ------------ | ------------------------------------------------------------------------ |
116
+ | `mongodb` | 27017 | available |
117
+ | `postgres` | 5432 | available |
118
+ | `mysql` | 3306 | coming soon (disabled in the wizard) |
117
119
  | `none` | — | opt out — no driver, no driver package, `src/config/database.ts` removed |
118
120
 
119
121
  **Features** (`--features`): `react`, `react-email`, `mail`, `ses`, `image`, `s3`, `redis`, `scheduler`, `herald`, `socket`, `test`, `access`, `web`, `tailwind`, `shadcn`, `notifications`.
@@ -182,3 +184,47 @@ Failures surface the error and halt; there is no rollback. After it finishes, `c
182
184
  - The `run-app` topic of the `warlock-js-core` skill — `warlock dev` / `build` / `start` once the project exists.
183
185
  - The `create-module` topic of the `warlock-js-core` skill — generate a new module inside the project.
184
186
  - The `add-connector` topic of the `warlock-js-core` skill — what `warlock add` wires when a feature pulls a connector.
187
+
188
+ The project-ready summary wraps the complete feature list to your terminal width.
189
+ Terminals narrower than 50 columns get a plain list; piped output uses 80 columns.
190
+
191
+ ## Custom feature groups
192
+
193
+ `--customize` opens a section menu for optional features. Open a section to edit
194
+ its checklist; **Select all** includes the remaining features while preserving
195
+ items you explicitly unticked. The category cursor stays on the last edited
196
+ section. Section
197
+ counts update when returning to the menu, and **Continue** keeps the selection.
198
+ Flags preselect their existing feature keys. Both menus and checklists retain
199
+ terminal paging. The decisions overview previews up to six feature/AI keys with
200
+ a total count; the success screen shows the complete list.
201
+
202
+ ## Choose the linter and formatter
203
+
204
+ `--lint=eslint` is the default. `--lint=oxc` (also `--lint oxc`) installs exact
205
+ `oxlint@1.88.0` and `oxfmt@0.73.0` versions, emits `.oxlintrc.json` and
206
+ `.oxfmtrc.json`, and removes the template's ESLint/Prettier configs and dependencies.
207
+ The custom wizard prompts for the same choice; a flag preselects it.
208
+
209
+ ```bash
210
+ npm create warlock@latest my-app -- --lint=oxc --yes
211
+ ```
212
+
213
+ Both choices provide `lint`/`format` and non-mutating `lint:check`/`format:check`
214
+ scripts. Oxc projects also configure VS Code format-on-save with the official
215
+ `oxc.oxc-vscode` formatter, use `source.fixAll.oxc` for lint fixes and recommend
216
+ that extension. The ESLint toolchain retains its Prettier/ESLint editor settings.
217
+ Oxc config enables `devServer.healthCheckers: { eslint: false, oxlint: true }` and
218
+ leaves TypeScript health checks enabled. Style matches the template's 100-column,
219
+ double-quote, semicolon and trailing-comma settings. Import sorting is on in
220
+ `.oxfmtrc.json`, kept to one block like the ESLint toolchain's
221
+ `prettier-plugin-organize-imports`:
222
+
223
+ ```json
224
+ { "sortImports": { "newlinesBetween": false } }
225
+ ```
226
+
227
+ `format` (`oxfmt --write ./src`) orders built-ins, then packages, then relative
228
+ imports. Oxfmt does not delete unused imports, so remove them from Oxlint's
229
+ `no-unused-vars` diagnostics. Scripts in both toolchains call local binaries
230
+ (`eslint --fix ./src --max-warnings=0`, `tsc --noEmit`), never `npx`.
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "semi": true,
4
+ "tabWidth": 2,
5
+ "printWidth": 100,
6
+ "singleQuote": false,
7
+ "arrowParens": "always",
8
+ "trailingComma": "all",
9
+ "endOfLine": "lf",
10
+ "sortImports": { "newlinesBetween": false },
11
+ "ignorePatterns": ["node_modules/", "dist/"]
12
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["typescript", "import", "react"],
4
+ "env": { "node": true, "es2021": true },
5
+ "categories": { "correctness": "off" },
6
+ "rules": {
7
+ "typescript/no-explicit-any": "off",
8
+ "no-unused-vars": [
9
+ "warn",
10
+ {
11
+ "vars": "all",
12
+ "varsIgnorePattern": "^_",
13
+ "args": "after-used",
14
+ "argsIgnorePattern": "^_"
15
+ }
16
+ ],
17
+ "typescript/explicit-member-accessibility": ["error", { "accessibility": "explicit" }],
18
+ "typescript/consistent-type-imports": [
19
+ "error",
20
+ { "prefer": "type-imports", "disallowTypeAnnotations": false }
21
+ ]
22
+ },
23
+ "ignorePatterns": ["node_modules/", "dist/"]
24
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["oxc.oxc-vscode"]
3
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "editor.defaultFormatter": "oxc.oxc-vscode",
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.oxc": "explicit",
6
+ "source.organizeImports": "never"
7
+ },
8
+ "[typescript]": {
9
+ "editor.defaultFormatter": "oxc.oxc-vscode",
10
+ "editor.formatOnSave": true
11
+ },
12
+ "[typescriptreact]": {
13
+ "editor.defaultFormatter": "oxc.oxc-vscode",
14
+ "editor.formatOnSave": true
15
+ },
16
+ "[javascript]": {
17
+ "editor.defaultFormatter": "oxc.oxc-vscode",
18
+ "editor.formatOnSave": true
19
+ },
20
+ "[javascriptreact]": {
21
+ "editor.defaultFormatter": "oxc.oxc-vscode",
22
+ "editor.formatOnSave": true
23
+ },
24
+ "[json]": {
25
+ "editor.defaultFormatter": "oxc.oxc-vscode",
26
+ "editor.formatOnSave": true
27
+ },
28
+ "[css]": {
29
+ "editor.defaultFormatter": "oxc.oxc-vscode",
30
+ "editor.formatOnSave": true
31
+ },
32
+ "oxc.enable.oxlint": true,
33
+ "oxc.enable.oxfmt": true,
34
+ "oxc.requireConfig": true
35
+ }
@@ -80,8 +80,8 @@ automatically on `postinstall`).
80
80
  - `yarn start` — start the production server (`warlock start`)
81
81
  - `yarn migrate` / `yarn migrate.fresh` / `yarn migrate.list` — run migrations
82
82
  - `yarn seed` — run database seeders
83
- - `yarn lint` / `yarn format` / `yarn tsc` — lint (eslint --fix), format
84
- (prettier), and type-check (`tsc --noEmit`)
83
+ - `yarn lint` / `yarn format` / `yarn tsc` — lint and format with the selected toolchain, and type-check (`tsc --noEmit`)
84
+ - `npx warlock check` — verify a change: typecheck, lint, the affected tests and the framework's conventions in one report (`--changed` for only what changed); run it after every change and loop until it exits 0
85
85
  - `yarn skills:sync` — re-sync skills from `node_modules` and regenerate the
86
86
  derived per-agent files from this `AGENTS.md`
87
87
 
@@ -98,6 +98,13 @@ suffixes, and JSDoc on the public surface. The bar is senior-level clean code, n
98
98
 
99
99
  - Respect **module-boundaries** — do not reach across module internals.
100
100
  - Never weaken the **security-baseline** — always validate input at boundaries.
101
+ - `npx warlock check` enforces these rules whichever linter the project uses:
102
+ pass `request.validated()` (never `request.all()`) to queries and services
103
+ (`WL9005`), give every POST/PUT/PATCH/DELETE route auth middleware or an
104
+ explicit `public: true` (`WL9006`), wrap request-supplied redirect targets in
105
+ `safeRedirect()` (`WL9007`), keep raw emails, tokens and request bodies out of
106
+ logs (`WL9008`) and bound regexes that run on request input (`WL9009`). Each
107
+ problem carries its fix; `npx warlock explain <CODE>` prints it again.
101
108
  - Follow **git-workflow** for commits and branches; do not commit or push unless
102
109
  explicitly asked.
103
110
  - When you are unsure how a package or feature works, **stop and read its skill**
@@ -122,3 +129,16 @@ Three things that trip everyone up on a fresh app:
122
129
  instance does not expose its columns as direct properties — `post.title` is
123
130
  `undefined` at runtime and a type error. Use `post.get("title")`, or
124
131
  `post.toJSON()` in a page loader (resources are the serialization gate).
132
+
133
+ ## This app (generated)
134
+
135
+ `warlock dev` rewrites everything between the two markers below from the
136
+ running app — modules, routes, models, installed skills and the commands to
137
+ verify a change. Edit outside the markers; turn it off with
138
+ `devServer.agentsMd: false` in `warlock.config.ts`.
139
+
140
+ <!-- warlock:generated:start -->
141
+
142
+ Run `npx warlock dev` (or `npx warlock agents-md --write`) to fill this section.
143
+
144
+ <!-- warlock:generated:end -->
@@ -7,7 +7,7 @@ Full documentation: [warlock.js.org](https://warlock.js.org).
7
7
 
8
8
  ## Requirements
9
9
 
10
- - Node.js **22.13** or newer
10
+ - Node.js **22.15** or newer
11
11
  - The database you picked when scaffolding (MongoDB or PostgreSQL), running and
12
12
  reachable with the settings in `.env` — skip this if you scaffolded with no
13
13
  database
@@ -40,8 +40,10 @@ It restarts on file changes.
40
40
  | `npm run seed` | Run the database seeders |
41
41
  | `npm run jwt` | Generate a JWT secret into `.env` |
42
42
  | `npm run tsc` | Type-check without building |
43
- | `npm run lint` | ESLint, auto-fixing what it can |
44
- | `npm run format` | Prettier over `src/` |
43
+ | `npm run lint` | The configured linter, auto-fixing what it can |
44
+ | `npm run format` | The configured formatter over `src/` |
45
+ | `npm run lint:check` | Lint without changing files |
46
+ | `npm run format:check` | Check formatting without changing files |
45
47
  | `npm run skills:sync` | Refresh the AI agent skills (see below) |
46
48
 
47
49
  The `warlock` CLI has more than the scripts expose — run `npx warlock --help`
@@ -19,12 +19,14 @@
19
19
  "gen.mig": "warlock generate.migration",
20
20
  "gen.r": "warlock generate.repository",
21
21
  "gen.rs": "warlock generate.resource",
22
- "lint": "npx eslint --fix ./src --max-warnings=0",
23
- "format": "npx prettier --write ./src/**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc.json",
24
- "tsc": "warlock generate.typings && npx tsc --noEmit",
22
+ "lint": "eslint --fix ./src --max-warnings=0",
23
+ "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,css,md,json}\" --config ./.prettierrc.json",
24
+ "tsc": "warlock generate.typings && tsc --noEmit",
25
25
  "postinstall": "agent-kit init && agent-kit sync || echo \"warning: AI agent skills were NOT synced, so coding agents will not see the @warlock.js skills. Run the skills:sync script.\"",
26
26
  "skills:sync": "agent-kit sync",
27
- "skills:watch": "agent-kit watch"
27
+ "skills:watch": "agent-kit watch",
28
+ "lint:check": "eslint ./src --max-warnings=0",
29
+ "format:check": "prettier --check \"./src/**/*.{js,jsx,ts,tsx,css,md,json}\" --config ./.prettierrc.json"
28
30
  },
29
31
  "dependencies": {
30
32
  "@mongez/reinforcements": "^4.0.2",
@@ -24,8 +24,8 @@ The list lands under the **plural name of the module's resource** — `users`, `
24
24
 
25
25
  ```typescript
26
26
  // ✅ list-users.controller.ts
27
- export const listUsersController: RequestHandler = async (request, response) => {
28
- const { data, pagination } = await listUsersService(request.all());
27
+ export const listUsersController: RequestHandler = async ({ request, response }) => {
28
+ const { data, pagination } = await listUsersService(request.validated());
29
29
 
30
30
  return response.success({
31
31
  users: data,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-standards
3
- description: 'Project-level TypeScript code standards for apps built on Warlock.js — `interface` vs `type`, access modifiers, no `any`, no inline-duplicated unions, file-naming suffixes (`.contract.ts` / `.type.ts` / `.service.ts` / `.model.ts` / `.resource.ts` / `.spec.ts`), single-responsibility files, FP-factory + internal-class pattern, brace-every-control-block readability with blank lines between consecutive blocks, JSDoc on public surface and non-obvious logic, error classes extending framework errors, async/await + `Promise.all`, `undefined` over `null`, Vitest co-located, ESLint + Prettier. Triggers: writing/editing any `.ts` / `.tsx` file in this project; user asks "what are the code standards / style rules / conventions"; spawning a sub-agent that will write code; reviewing a diff for style issues; deciding `interface` vs `type`, when to use a class, where helpers live, how to name a file, or whether something needs JSDoc. Skip: pure-Markdown / JSON / YAML edits with no code change; runtime / behavior questions about a specific framework primitive — load the relevant `@warlock.js/<pkg>` skill instead; package-publishing or CI workflow questions.'
3
+ description: 'Project-level TypeScript code standards for apps built on Warlock.js — `interface` vs `type`, access modifiers, no `any`, no inline-duplicated unions, file-naming suffixes (`.contract.ts` / `.type.ts` / `.service.ts` / `.model.ts` / `.resource.ts` / `.spec.ts`), single-responsibility files, FP-factory + internal-class pattern, brace-every-control-block readability with blank lines between consecutive blocks, JSDoc on public surface and non-obvious logic, error classes extending framework errors, async/await + `Promise.all`, `undefined` over `null`, Vitest co-located, ESLint/Prettier or Oxlint/Oxfmt. Triggers: writing/editing any `.ts` / `.tsx` file in this project; user asks "what are the code standards / style rules / conventions"; spawning a sub-agent that will write code; reviewing a diff for style issues; deciding `interface` vs `type`, when to use a class, where helpers live, how to name a file, or whether something needs JSDoc. Skip: pure-Markdown / JSON / YAML edits with no code change; runtime / behavior questions about a specific framework primitive — load the relevant `@warlock.js/<pkg>` skill instead; package-publishing or CI workflow questions.'
4
4
  ---
5
5
 
6
6
  # Code Standards
@@ -551,7 +551,20 @@ A secret with no safe default (a payment key, a signing secret) should fail loud
551
551
 
552
552
  ## 11. Tooling
553
553
 
554
- Ship a `.eslintrc` with at least these rules on:
554
+ Use the project's selected toolchain: `eslint.config.js` + `.prettierrc.json`, or
555
+ `.oxlintrc.json` + `.oxfmtrc.json`. Run `npm run lint:check` and
556
+ `npm run format:check` for non-mutating checks. The Oxc scaffold enables the
557
+ Oxlint development health checker and retains TypeScript checking.
558
+
559
+ `npx warlock check` is the one verify command, in either toolchain: it
560
+ typechecks, runs the configured linter and the affected tests, and checks the
561
+ framework's conventions — `request.validated()` instead of `request.all()`,
562
+ guarded or `public: true` mutating routes, safe redirects, redacted logs,
563
+ bounded regexes on input, one controller per file, `type` (not `kind`) as a
564
+ union's discriminator. Each problem has a `WL` code and its fix
565
+ (`npx warlock explain <CODE>`).
566
+
567
+ For ESLint, consider enabling these additional rules:
555
568
 
556
569
  - `@typescript-eslint/explicit-member-accessibility` — enforces § 1.2
557
570
  - `@typescript-eslint/no-explicit-any` — enforces § 1.3
@@ -561,7 +574,7 @@ Ship a `.eslintrc` with at least these rules on:
561
574
  - `curly` — enforces braces (§ 4.4)
562
575
  - `no-console` — enforces § 8
563
576
 
564
- Match the project's `.prettierrc` exactly. If two configs are present and disagree, the one closer to the file wins (Prettier's resolution order). Don't fight the formatter — configure it, then trust it.
577
+ Match the selected formatter config: `.prettierrc.json` or `.oxfmtrc.json`. Both use 100 columns, double quotes, semicolons and trailing commas. Oxfmt does not run `prettier-plugin-organize-imports`; maintain import order and remove unused imports separately, then run the linter.
565
578
 
566
579
  ---
567
580
 
@@ -583,7 +596,8 @@ Before any change ships:
583
596
  - [ ] JSDoc on every public export and every multi-method class
584
597
  - [ ] No `console.log`, `log.<level>(module, action, message, context)` signature used correctly
585
598
  - [ ] Tests co-located, public-surface-driven
586
- - [ ] `tsc --noEmit` and `eslint` pass clean
599
+ - [ ] `tsc --noEmit`, `npm run lint:check` and `npm run format:check` pass clean
600
+ - [ ] `npx warlock check` exits 0
587
601
 
588
602
  **Non-negotiable defaults**
589
603
  - [ ] Every untrusted input has a `@warlock.js/seal` schema at the boundary
@@ -243,7 +243,17 @@ The framework sets `Content-Type: application/json` for `response.success(...)`
243
243
 
244
244
  ### 5.3 Open redirects
245
245
 
246
- If you implement a "redirect back to where the user came from" feature, validate the destination URL against a whitelist of internal paths. Never blindly follow a user-supplied URL.
246
+ If you implement a "redirect back to where the user came from" feature, validate the destination URL against a whitelist of internal paths. Never blindly follow a user-supplied URL. `safeRedirect()` from `@warlock.js/core` does it: same-origin paths and the origins you allow pass, anything else becomes the fallback.
247
+
248
+ ```typescript
249
+ import { safeRedirect, type RequestHandler } from "@warlock.js/core";
250
+
251
+ export const afterLoginController: RequestHandler = async ({ request, response }) => {
252
+ return response.redirect(safeRedirect(request.input("next"), { fallback: "/dashboard" }));
253
+ };
254
+ ```
255
+
256
+ `npx warlock check` reports a redirect target taken from request input without it as `WL9007`.
247
257
 
248
258
  ### 5.4 CORS
249
259
 
@@ -350,3 +360,4 @@ Before merging a change that touches input, identity, secrets, or persistence:
350
360
  - [ ] Logger redaction config covers any new sensitive field introduced
351
361
  - [ ] File uploads cap size, sniff MIME, server-generate the stored filename
352
362
  - [ ] Webhooks verify signature before any other work
363
+ - [ ] `npx warlock check` reports no `WL9005`–`WL9009` problem (unvalidated `request.all()`, unguarded mutating route, unsafe redirect, sensitive value in a log, unbounded regex on input)
@@ -170,6 +170,23 @@ vi.advanceTimersByTime(1000);
170
170
 
171
171
  A test that sleeps is a test that's slow and flaky. Stub the clock.
172
172
 
173
+ Code that reads time through `clock.now()` / `clock.timestamp()` from `@warlock.js/core` gets a frozen clock in tests already (`clockFake.travel(ms)` moves it), with no fake timers involved.
174
+
175
+ ### 4.5 Mail, queue, cache, storage — fakes are already on
176
+
177
+ In the test environment `setupTest()` installs fakes for mail, queue, cache, storage, clock and ids. Assert on them instead of mocking the drivers:
178
+
179
+ ```typescript
180
+ import { mailFake, queueFake } from "@warlock.js/core/tests";
181
+
182
+ const order = { id: 42 }; // what the code under test placed
183
+
184
+ mailFake.assertSent({ to: "ada@example.test" });
185
+ queueFake.assertPushed("invoices.send", { orderId: order.id });
186
+ ```
187
+
188
+ Fixtures come from `defineFactory()` (deterministic, no faker). Opt out of a fake in `src/config/tests.ts` (`fakes: { cache: false }`). `warlock test --changed` runs only the tests a change affects. See the `test-fakes` and `model-factories` skills of `@warlock.js/core`.
189
+
173
190
  ---
174
191
 
175
192
  ## 5. AAA structure
@@ -1,3 +1,8 @@
1
+ import {
2
+ loginThrottleMiddleware,
3
+ tokenConsumeThrottleMiddleware,
4
+ tokenIssueThrottleMiddleware,
5
+ } from "@warlock.js/auth";
1
6
  import { router } from "@warlock.js/core";
2
7
  import { guarded } from "app/shared/utils/router";
3
8
  import { forgotPasswordController } from "./controllers/forgot-password.controller";
@@ -8,12 +13,27 @@ import { meController } from "./controllers/me.controller";
8
13
  import { refreshTokenController } from "./controllers/refresh-token.controller";
9
14
  import { resetPasswordController } from "./controllers/reset-password.controller";
10
15
 
11
- // Auth routes
16
+ // Auth routes. Sign-in and password recovery are open by design (`public: true`)
17
+ // and throttled; the rest need a signed-in user.
12
18
  router.prefix("/auth", () => {
13
- router.post("/login", loginController);
14
- router.post("/refresh-token", refreshTokenController);
15
- router.post("/forgot-password", forgotPasswordController);
16
- router.post("/reset-password", resetPasswordController);
19
+ // Failed sign-ins lock the account and the IP after 5 tries in 15 minutes.
20
+ router.post("/login", loginController, {
21
+ public: true,
22
+ middleware: [loginThrottleMiddleware()],
23
+ });
24
+ router.post("/refresh-token", refreshTokenController, {
25
+ public: true,
26
+ rateLimit: { max: 30, timeWindow: 60_000 },
27
+ });
28
+ // Reset mails: 3 per hour per email and per IP. Reset attempts: 10 failures per 15 minutes.
29
+ router.post("/forgot-password", forgotPasswordController, {
30
+ public: true,
31
+ middleware: [tokenIssueThrottleMiddleware()],
32
+ });
33
+ router.post("/reset-password", resetPasswordController, {
34
+ public: true,
35
+ middleware: [tokenConsumeThrottleMiddleware()],
36
+ });
17
37
  guarded(() => {
18
38
  router.post("/logout", logoutController);
19
39
  router.post("/logout-all", logoutAllController);
@@ -7,4 +7,9 @@ export type OTPChannel = "email" | "sms" | "whatsapp" | "push";
7
7
  * Common OTP types
8
8
  */
9
9
  export type OTPType =
10
- "password-reset" | "email-verification" | "phone-verification" | "two-factor" | string;
10
+ | "password-reset"
11
+ | "email-verification"
12
+ | "phone-verification"
13
+ | "two-factor"
14
+ // Applications may also define their own OTP purposes.
15
+ | string;
@@ -1,4 +1,4 @@
1
1
  import { router } from "@warlock.js/core";
2
2
  import { contactController } from "./controllers/contact.controller";
3
3
 
4
- router.post("/api/contact", contactController);
4
+ router.post("/api/contact", contactController, { public: true });
@@ -1,4 +1,4 @@
1
1
  import { router } from "@warlock.js/core";
2
2
  import { localeController } from "./controllers/locale.controller";
3
3
 
4
- router.post("/api/locale", localeController);
4
+ router.post("/api/locale", localeController, { public: true });
@@ -1,8 +1,12 @@
1
1
  import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
2
  import { usersRepository } from "../repositories/users.repository";
3
+ import { type ListUsersSchema, listUsersSchema } from "../schema/list-users.schema";
3
4
 
4
- export const listUsersController: GuardedRequestHandler = async ({ request, response }) => {
5
- const users = await usersRepository.listCached(request.all());
5
+ export const listUsersController: GuardedRequestHandler<ListUsersSchema> = async ({
6
+ request,
7
+ response,
8
+ }) => {
9
+ const users = await usersRepository.listCached(request.validated());
6
10
 
7
11
  return response.success({
8
12
  users,
@@ -10,3 +14,7 @@ export const listUsersController: GuardedRequestHandler = async ({ request, resp
10
14
  };
11
15
 
12
16
  listUsersController.description = "List Users Controller";
17
+
18
+ listUsersController.validation = {
19
+ schema: listUsersSchema,
20
+ };
@@ -0,0 +1,15 @@
1
+ import { type Infer, v } from "@warlock.js/seal";
2
+
3
+ /**
4
+ * Query parameters the user list accepts. Unknown keys are stripped, so a
5
+ * smuggled operator (`?email[$ne]=`) never reaches the repository filters.
6
+ */
7
+ export const listUsersSchema = v.object({
8
+ id: v.string().optional(),
9
+ name: v.string().max(100).optional(),
10
+ email: v.string().max(255).optional(),
11
+ page: v.numeric().min(1).optional(),
12
+ limit: v.numeric().min(1).max(100).optional(),
13
+ });
14
+
15
+ export type ListUsersSchema = Infer<typeof listUsersSchema>;
@@ -1,5 +1,13 @@
1
1
  const testsConfigurations = {
2
+ // In tests, mail, queue, cache, storage, clock and ids are faked by default:
3
+ // nothing is delivered, pushed to Redis or uploaded, and assertions such as
4
+ // mailFake.assertSent(...) work (see @warlock.js/core/tests).
2
5
  //
6
+ // Opt out of every fake, keeping the real drivers:
7
+ // fakes: false,
8
+ //
9
+ // ...or of one, e.g. keep the configured cache driver:
10
+ // fakes: { cache: false },
3
11
  };
4
12
 
5
13
  export default testsConfigurations;
@@ -14,12 +14,8 @@
14
14
  padding-inline: clamp(1.5rem, 7vw, 8rem);
15
15
  background: #fafafa;
16
16
  color: #18181b;
17
- font-family:
18
- system-ui,
19
- -apple-system,
20
- BlinkMacSystemFont,
21
- "Segoe UI",
22
- sans-serif;
17
+ /* prettier-ignore */
18
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
23
19
  }
24
20
 
25
21
  .not-found__content {