@dokebi/cli 0.4.1 → 0.4.26

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 (188) hide show
  1. package/AI.md +262 -57
  2. package/README.md +35 -10
  3. package/catalog.json +2519 -257
  4. package/describe.schema.json +748 -0
  5. package/dist/api-app.d.ts +2 -0
  6. package/dist/api-app.js +35 -0
  7. package/dist/app-urls.js +3 -2
  8. package/dist/bin.js +527 -35
  9. package/dist/capability-guide.d.ts +30 -0
  10. package/dist/capability-guide.js +122 -0
  11. package/dist/catalog.d.ts +16 -2
  12. package/dist/catalog.js +19 -7
  13. package/dist/check/asset-rules.d.ts +6 -0
  14. package/dist/check/asset-rules.js +163 -0
  15. package/dist/check/browser-sources.d.ts +20 -0
  16. package/dist/check/browser-sources.js +89 -0
  17. package/dist/check/console-rules.d.ts +6 -0
  18. package/dist/check/console-rules.js +40 -0
  19. package/dist/check/error-rules.d.ts +21 -0
  20. package/dist/check/error-rules.js +212 -0
  21. package/dist/check/form-rules.d.ts +12 -0
  22. package/dist/check/form-rules.js +69 -6
  23. package/dist/check/formatter-rules.d.ts +2 -0
  24. package/dist/check/formatter-rules.js +71 -0
  25. package/dist/check/i18n-rules.d.ts +27 -0
  26. package/dist/check/i18n-rules.js +353 -0
  27. package/dist/check/index.d.ts +15 -1
  28. package/dist/check/index.js +89 -11
  29. package/dist/check/mail-rules.d.ts +4 -0
  30. package/dist/check/mail-rules.js +72 -0
  31. package/dist/check/module-rules.d.ts +13 -0
  32. package/dist/check/module-rules.js +104 -0
  33. package/dist/check/packages.d.ts +8 -0
  34. package/dist/check/packages.js +16 -0
  35. package/dist/check/pool-rules.d.ts +8 -0
  36. package/dist/check/pool-rules.js +164 -0
  37. package/dist/check/problems.d.ts +6 -1
  38. package/dist/check/problems.js +100 -2
  39. package/dist/check/project-rules.d.ts +23 -3
  40. package/dist/check/project-rules.js +265 -23
  41. package/dist/check/seal-rules.d.ts +6 -0
  42. package/dist/check/seal-rules.js +68 -0
  43. package/dist/check/security-rules.d.ts +10 -0
  44. package/dist/check/security-rules.js +450 -0
  45. package/dist/check/shared-state-rules.d.ts +2 -0
  46. package/dist/check/shared-state-rules.js +114 -0
  47. package/dist/check/source-rules.d.ts +26 -3
  48. package/dist/check/source-rules.js +249 -28
  49. package/dist/check/structure-rules.d.ts +23 -0
  50. package/dist/check/structure-rules.js +502 -0
  51. package/dist/check/timer-rules.d.ts +7 -0
  52. package/dist/check/timer-rules.js +543 -0
  53. package/dist/check/typescript.d.ts +35 -5
  54. package/dist/check/typescript.js +163 -17
  55. package/dist/check/vite-rules.d.ts +2 -0
  56. package/dist/check/vite-rules.js +136 -0
  57. package/dist/check/wait-rules.d.ts +16 -0
  58. package/dist/check/wait-rules.js +337 -0
  59. package/dist/commands.d.ts +7 -0
  60. package/dist/commands.js +307 -31
  61. package/dist/config-diff.d.ts +42 -0
  62. package/dist/config-diff.js +289 -0
  63. package/dist/config-edit.d.ts +35 -2
  64. package/dist/config-edit.js +145 -13
  65. package/dist/config-init.d.ts +15 -0
  66. package/dist/config-init.js +61 -0
  67. package/dist/config-paths.d.ts +22 -0
  68. package/dist/config-paths.js +82 -0
  69. package/dist/config-show.d.ts +82 -0
  70. package/dist/config-show.js +563 -0
  71. package/dist/config-values.d.ts +8 -0
  72. package/dist/config-values.js +68 -0
  73. package/dist/configuration-scaffold.d.ts +42 -0
  74. package/dist/configuration-scaffold.js +258 -4
  75. package/dist/configuration.d.ts +63 -3
  76. package/dist/configuration.js +178 -4
  77. package/dist/deployment.d.ts +2 -0
  78. package/dist/deployment.js +111 -0
  79. package/dist/describe-db.d.ts +22 -0
  80. package/dist/describe-db.js +176 -0
  81. package/dist/describe.d.ts +92 -0
  82. package/dist/describe.js +417 -0
  83. package/dist/dev-compose.d.ts +20 -0
  84. package/dist/dev-compose.js +154 -0
  85. package/dist/dev-guard.js +44 -0
  86. package/dist/dev-host.d.ts +13 -0
  87. package/dist/dev-host.js +55 -0
  88. package/dist/dev-infra.d.ts +72 -4
  89. package/dist/dev-infra.js +237 -17
  90. package/dist/dev-watch.d.ts +34 -0
  91. package/dist/dev-watch.js +192 -0
  92. package/dist/diagnostics.d.ts +10 -7
  93. package/dist/diagnostics.js +22 -4
  94. package/dist/drift.d.ts +50 -0
  95. package/dist/drift.js +218 -0
  96. package/dist/env-file.d.ts +25 -1
  97. package/dist/env-file.js +64 -6
  98. package/dist/errors.d.ts +7 -1
  99. package/dist/example-config.d.ts +2 -0
  100. package/dist/example-config.js +3 -0
  101. package/dist/first-admin.d.ts +43 -0
  102. package/dist/first-admin.js +175 -0
  103. package/dist/formatter-ignore.d.ts +37 -0
  104. package/dist/formatter-ignore.js +344 -0
  105. package/dist/generator.d.ts +45 -3
  106. package/dist/generator.js +205 -15
  107. package/dist/guide.d.ts +1 -1
  108. package/dist/guide.js +42 -21
  109. package/dist/install-module.d.ts +29 -3
  110. package/dist/install-module.js +142 -13
  111. package/dist/install-plan.js +5 -3
  112. package/dist/layout.d.ts +12 -1
  113. package/dist/layout.js +19 -6
  114. package/dist/log-writer-control.d.ts +2 -0
  115. package/dist/log-writer-control.js +106 -27
  116. package/dist/log-writer.js +43 -10
  117. package/dist/logging-config.d.ts +8 -3
  118. package/dist/logging-config.js +19 -5
  119. package/dist/manifest.d.ts +29 -1
  120. package/dist/manifest.js +84 -6
  121. package/dist/merge-install-defaults.d.ts +0 -1
  122. package/dist/merge-install-defaults.js +13 -47
  123. package/dist/npm-client.d.ts +59 -4
  124. package/dist/npm-client.js +220 -16
  125. package/dist/npm-paths.js +18 -4
  126. package/dist/openapi.d.ts +27 -0
  127. package/dist/openapi.js +649 -0
  128. package/dist/option-fixes.d.ts +39 -0
  129. package/dist/option-fixes.js +130 -0
  130. package/dist/output.d.ts +1 -1
  131. package/dist/output.js +5 -5
  132. package/dist/plugin-lifecycle.d.ts +34 -2
  133. package/dist/plugin-lifecycle.js +206 -20
  134. package/dist/plugin-options.d.ts +19 -4
  135. package/dist/plugin-options.js +138 -6
  136. package/dist/plugins-command.js +44 -7
  137. package/dist/process-group.d.ts +39 -0
  138. package/dist/process-group.js +132 -0
  139. package/dist/project-guide.d.ts +19 -1
  140. package/dist/project-guide.js +119 -20
  141. package/dist/project-inspector.d.ts +15 -0
  142. package/dist/project-inspector.js +481 -33
  143. package/dist/project-state.d.ts +5 -0
  144. package/dist/project-state.js +8 -0
  145. package/dist/project.d.ts +203 -19
  146. package/dist/project.js +3016 -336
  147. package/dist/recipe-facts.d.ts +127 -0
  148. package/dist/recipe-facts.js +563 -0
  149. package/dist/recipe-login.d.ts +63 -0
  150. package/dist/recipe-login.js +464 -0
  151. package/dist/recipe-mail.d.ts +21 -0
  152. package/dist/recipe-mail.js +214 -0
  153. package/dist/recipe-members.d.ts +35 -0
  154. package/dist/recipe-members.js +178 -0
  155. package/dist/recipe-record.d.ts +82 -7
  156. package/dist/recipe-record.js +226 -22
  157. package/dist/recipe-reset.d.ts +2 -0
  158. package/dist/recipe-reset.js +117 -0
  159. package/dist/recipe-site.d.ts +48 -0
  160. package/dist/recipe-site.js +396 -0
  161. package/dist/recipe-step.d.ts +42 -0
  162. package/dist/recipe-step.js +89 -0
  163. package/dist/recipe.d.ts +56 -20
  164. package/dist/recipe.js +703 -131
  165. package/dist/registry.d.ts +98 -2
  166. package/dist/registry.js +171 -5
  167. package/dist/reloadable.d.ts +35 -0
  168. package/dist/reloadable.js +257 -0
  169. package/dist/server.d.ts +50 -4
  170. package/dist/server.js +790 -103
  171. package/dist/settings.d.ts +48 -0
  172. package/dist/settings.js +168 -0
  173. package/dist/site-auth.d.ts +39 -0
  174. package/dist/site-auth.js +195 -0
  175. package/dist/template-fixes.d.ts +26 -0
  176. package/dist/template-fixes.js +285 -0
  177. package/dist/test-reporter.d.ts +49 -0
  178. package/dist/test-reporter.js +190 -0
  179. package/dist/transaction.d.ts +67 -0
  180. package/dist/transaction.js +428 -46
  181. package/dist/verify-tests.d.ts +98 -0
  182. package/dist/verify-tests.js +449 -0
  183. package/dist/verify.d.ts +113 -4
  184. package/dist/verify.js +326 -28
  185. package/dist/view-choice.d.ts +58 -0
  186. package/dist/view-choice.js +131 -0
  187. package/dist/vite-watch.js +4 -2
  188. package/package.json +8 -6
@@ -1,3 +1,4 @@
1
+ import { ANSWER_SOURCES, ASK_AND_WAIT, BROWSER_APPS, capabilityBlock, markdownFlags, } from './capability-guide.js';
1
2
  /** Markers of the CLI-maintained list of installed packages in AGENTS.md (D-0065). */
2
3
  export const INSTALLED_START = '<!-- dokebi:installed:start -->';
3
4
  export const INSTALLED_END = '<!-- dokebi:installed:end -->';
@@ -5,6 +6,13 @@ export const AGENTS_FILE = 'AGENTS.md';
5
6
  export const CLAUDE_FILE = 'CLAUDE.md';
6
7
  /** CLAUDE.md of a new project: Claude Code reads AGENTS.md through this import line. */
7
8
  export const CLAUDE_GUIDE = '@AGENTS.md\n';
9
+ /**
10
+ * Markers of the project's own block in AGENTS.md (D-0077 G): the people of the project (or their
11
+ * tools) write rules there; the CLI never changes what is between them and doctor never judges it.
12
+ */
13
+ export const PROJECT_START = '<!-- dokebi:project:start -->';
14
+ export const PROJECT_END = '<!-- dokebi:project:end -->';
15
+ const PROJECT_HEADING = '## This project';
8
16
  /** The block between the markers (markers included), rewritten by add, upgrade and remove. */
9
17
  export function installedBlock(entries) {
10
18
  const rows = entries.map((entry) => `| ${entry.id} | ${entry.packageName} | ${entry.version} | ${entry.documentation ? `node_modules/${entry.packageName}/${entry.documentation}` : '-'} |`);
@@ -79,8 +87,54 @@ export function replaceInstalledBlock(text, block) {
79
87
  return undefined;
80
88
  return text.slice(0, start) + block + text.slice(end + INSTALLED_END.length);
81
89
  }
90
+ /** The project block as init writes it (and as it is added to an older AGENTS.md). */
91
+ export function projectBlock() {
92
+ return [
93
+ PROJECT_START,
94
+ '<!-- This block belongs to the project: write its own rules and notes here. The Dokebi CLI never changes what is between these two markers. -->',
95
+ PROJECT_END,
96
+ ].join('\n');
97
+ }
98
+ /** How many times `marker` stands in `text`, and where the first one is. */
99
+ function find(text, marker) {
100
+ let count = 0;
101
+ let at = -1;
102
+ for (let index = text.indexOf(marker); index !== -1; index = text.indexOf(marker, index + 1)) {
103
+ if (at === -1)
104
+ at = index;
105
+ count++;
106
+ }
107
+ return { count, at };
108
+ }
109
+ /**
110
+ * The state of the project block in an AGENTS.md text: `ok` (one start, then one end, outside
111
+ * the installed list), `missing` (no marker at all: the next add, upgrade or remove adds an
112
+ * empty block) or `invalid` (a marker alone, repeated, out of order, or inside the installed
113
+ * list: doctor warns GUIDE_PROJECT_BLOCK_INVALID and the CLI leaves the file alone).
114
+ */
115
+ export function projectBlockState(text) {
116
+ const start = find(text, PROJECT_START);
117
+ const end = find(text, PROJECT_END);
118
+ if (start.count === 0 && end.count === 0)
119
+ return 'missing';
120
+ if (start.count !== 1 || end.count !== 1 || end.at < start.at)
121
+ return 'invalid';
122
+ const installedStart = text.indexOf(INSTALLED_START);
123
+ const installedEnd = text.indexOf(INSTALLED_END);
124
+ if (installedStart !== -1 &&
125
+ installedEnd !== -1 &&
126
+ start.at < installedEnd + INSTALLED_END.length &&
127
+ end.at + PROJECT_END.length > installedStart)
128
+ return 'invalid';
129
+ return 'ok';
130
+ }
131
+ /** The text with an empty project block (and its heading) added at the end. */
132
+ export function withProjectBlock(text) {
133
+ const body = text.endsWith('\n') ? text : `${text}\n`;
134
+ return `${body}\n${PROJECT_HEADING}\n\n${projectBlock()}\n`;
135
+ }
82
136
  /** AGENTS.md of a new project (D-0065 skeleton): ask first, use the catalog, verify last. */
83
- export function projectGuide(entries) {
137
+ export function projectGuide(entries, catalog) {
84
138
  return `# Working on this Dokebi project (for AI agents)
85
139
 
86
140
  This project is built with the Dokebi framework. The CLI is the \`@dokebi/cli\` installed in this project; run it from the project root as \`npx dokebi …\`.
@@ -92,52 +146,97 @@ Run the command-line tools installed in this project by their bin names from the
92
146
  1. \`npx dokebi guide --json\`: the steps from a one-line request to a verified app (this document in short) and every command.
93
147
  2. \`npx dokebi inspect --json\`: installed plugins, versions, option schemas and diagnostics.
94
148
  3. Read the AI guide of every package in the "Installed packages" table below (the AI.md of the installed version). Read a plugin's AI.md before you write code with it (\`npx dokebi plugins doc <id> --json\`). The installed AI.md is right where older documents on the internet or your memory disagree.
149
+ 4. Read "This project" at the end of this file: the rules and notes of this project, written by its people. Follow them too.
150
+ 5. Browser apps and the view (D-0098): ${BROWSER_APPS}
95
151
 
96
152
  ## 1. Do not decide alone: ask first
97
153
 
98
- - Find the recipe for the request: \`npx dokebi recipe list --json\` (a blog is \`blog\`, a JSON API is \`api\`).
154
+ - Find the recipe for the request: \`npx dokebi recipe list --json\`. A service people use in a browser is \`site\` (or \`blog\` for posts), which ask the view (default Inertia + Vue); \`api\` builds no browser screens and is for an API the user asked for, whose clients are other programs (its result says so in \`notice\`).
99
155
  - When answers are missing, \`npx dokebi recipe <name> --json\` installs nothing and returns \`status: "needs-answers"\`, \`waitForUser: true\` and \`questions[]\` (options, pros and cons, what each installs, what it needs, the default and its reason), with the error \`ANSWERS_REQUIRED\` (exit 2).
100
- - Choices such as the view (ejs, inertia-vue, inertia-vue-ssr), the database (postgres, mysql, mariadb, mongodb, none), the development database (docker, url) and an admin area are **the user's: show them as questions and get the answers**. Tell the user that an option with \`available: false\` (absent) does not exist; do not offer it.
101
- - **Ask these questions and end your turn; do not run recipe/add/config until the user answers. Only use \`--defaults\` if the user said to decide.** After showing the questions, do not go on installing ("I will continue with the defaults while you choose").
102
- - Use \`--defaults\` only when the user left the choice to you ("you decide"). Then report the chosen values and their \`defaultReason\` to the user when you are done (\`verify\` returns them as \`tellUser\`, see 3).
103
- - When the user answers, pass the answers as flags and add \`--answers-from user\`. Look at the plan first with \`--dry-run --json\` (\`plan.steps\`, \`preflight\`), then run it (applying without \`--answers-from user\` or \`--defaults\` fails with \`ANSWERS_SOURCE_REQUIRED\`). Tell the user the \`report\` of the result (where the answers came from, the defaults, and what to know, such as no CSRF protection). On a preflight error (\`DOCKER_MISSING\`, \`PORT_IN_USE\`) ask the user; never switch to another database or port behind their back.
156
+ - Choices such as the view (ejs, inertia-vue, inertia-vue-ssr), the database (postgres, mysql, mariadb, mongodb, none), the development database (docker, url) development mail and an admin area are **the user's: show them as questions and get the answers**. Tell the user that an option with \`available: false\` (absent) does not exist; do not offer it.
157
+ - **${markdownFlags(ASK_AND_WAIT)}** After showing the questions, do not go on installing ("I will continue with the defaults while you choose").
158
+ - Use \`--defaults\` or \`--ai-chose\` only when the user left the choice to you ("you decide"). Then report the chosen values and their reasons to the user when you are done (\`verify\` returns them as \`tellUser\`, see 3).
159
+ - A request often settles some choices and leaves others open (D-0095). ${ANSWER_SOURCES}
160
+ - When the user answers, pass the answers as flags and add \`--answers-from user\`. Look at the plan first with \`--dry-run --json\` (\`plan.steps\`, \`preflight\`), then run it (applying without \`--answers-from user\` or \`--defaults\` fails with \`ANSWERS_SOURCE_REQUIRED\`). Tell the user the \`report\` of the result (where the answers came from, the defaults, and what to know, such as how the forms send the CSRF token and where the admin login is). On a preflight error (\`DOCKER_MISSING\`, \`PORT_IN_USE\`) ask the user; never switch to another database or port behind their back.
104
161
 
105
162
  ## 2. Plugins
106
163
 
107
164
  - What exists: \`npx dokebi plugins list --json\`; find: \`npx dokebi plugins search <word|capability> --json\`; details: \`npx dokebi plugins info <id> --json\`.
108
- - The \`absent\` entries of a search (for example auth, csrf, upload, mail, markdown, sqlite, React) are **features Dokebi does not have**. Never invent a plugin, flag or API for them: build them in app code as their \`instead\` says, or tell the user.
165
+ - The \`absent\` entries returned by the current catalog are **features Dokebi does not have**. Use that catalog rather than a stale hard-coded feature list. Never invent a plugin, flag or API for them: build them in app code as their \`instead\` says, or tell the user.
109
166
  - Install with \`npx dokebi add <package> --dry-run --json\` first, then the same command without \`--dry-run\`. Without a version, add picks the newest version that fits this core (\`resolved\`).
110
- - Never install or run a package outside \`@dokebi/\` (someone else's package, \`THIRD_PARTY_PACKAGE\`) without the permission of the user.
111
- - Never pass a secret (password, token, connection URL) with \`--set\` or \`config set\` (the shell history keeps it). Write it into dokebi.yaml yourself, use \`npx dokebi config secret <id>.<path>\` for a generated one, or use the value \`npx dokebi dev-infra\` generated.
112
- - Secrets a plugin reads from an environment variable (for example \`DOKEBI_SESSION_SECRETS\` of @dokebi/session) live in \`.env\`: \`npx dokebi add\` generates the ones a plugin declares (\`generatedSecrets\` names them), and \`npx dokebi dev\`, \`start\` and \`verify\` pass \`.env\` to the app (a variable set in the environment wins). Do not wrap the CLI in \`node --env-file\`, never commit \`.env\` and never print its values.
167
+ - Never install or run a package outside \`@dokebi/\` (someone else's package, \`THIRD_PARTY_PACKAGE\`) without the permission of the user. Two kinds of development-only tool are already asked for by these rules and need no separate permission: a formatter for the source formats (section 3) and a headless browser runner for browser checks. Add them as devDependencies, keep downloaded browsers out of the source tree and Git, and name both in your final report. The tools a \`dokebi\` command installs as part of its scaffold belong to the framework and need no permission either: \`dokebi app add --inertia\` installs \`vite\`, \`@vitejs/plugin-vue\`, \`vue\`, \`@inertiajs/vue3\` and the Vue type checker \`vue-tsc\` with \`@typescript/typescript6\` (the TypeScript 6 API it runs on). Anything else outside \`@dokebi/\` (a runtime library, a different database driver, another framework) still needs the user's permission.
168
+ - **Secrets live in dokebi.yaml** (0600, kept out of Git, no environment-variable references), never in dokebi.example.yaml: \`npx dokebi add\` writes the secrets a plugin declares (\`generatedSecrets\` names them, never the values), \`npx dokebi config secret <id>.<path>\` generates one later, \`npx dokebi dev-infra\` writes the development database URL, and a value the user supplies (such as a database URL with a password) is written with your file editor or \`npx dokebi config set\`, or by the user. An environment variable is only an optional production override where a plugin documents one. A fresh copy of the project (a clone, a CI checkout) has no dokebi.yaml: \`npx dokebi config init\` writes it from the sample with new generated secrets and names the next steps (D-0163).
169
+ - **dokebi.yaml is the developer's file** (D-0165): read it and edit it directly, secrets included; \`--set\` and \`config set\` take secrets too. What stays out of Git is dokebi.yaml itself (git-ignored; \`npx dokebi check\` reports \`secret-in-config\` when it could be committed) and any secret in dokebi.example.yaml.
170
+ - \`npx dokebi config show [<id>[.<path>]] --json\` gives the options of dokebi.yaml with schema defaults, every secret shown as \`<secret: set>\` (for output you share). To keep dokebi.example.yaml in step, \`npx dokebi config diff --json\` names the option paths the two files do not share. To replace a secret, \`npx dokebi config secret <id>.<path> --rotate\` (a list keeps the previous two after the new one, so what they signed still verifies; restart the app).
171
+ - An existing \`.env\` is still passed to the app by \`npx dokebi dev\`, \`start\` and \`verify\`; do not create one for secrets.
172
+ - Docker infrastructure uses the same YAML password: prepare with \`npx dokebi dev-infra\`, then \`npx dokebi dev-infra up\`. No .env is generated. Use repeated \`--file <path>\` for multiple Compose files; start every required database, Redis, NATS and mail service before the app. Stopping an evaluation's containers makes its saved app unusable until those services are started again. Preserve the database password and volumes on migration. A MongoDB from \`dev-infra\` is a single-node replica set (\`rs0\`), so multi-document transactions work; never hand-edit compose.yaml to get one, and when \`dev-infra\` warns \`DEV_INFRA_STANDALONE\` (a compose.yaml written before), follow its \`hint\` to switch.
173
+ - **To see what the app has, run \`npx dokebi describe --json\`**: every route (with \`id\`, schema and the login rule of @dokebi/auth), the pages (EJS views, Inertia components), jobs, schedules, the error codes of \`src/shared/errors.ts\`, the option keys and the environment variable names, never a value. It loads the app without serving or connecting; read it before you change code you did not write. Before you write a migration, run \`npx dokebi describe --with-db --json\` to see the current tables (it connects to each database); \`npx dokebi describe --openapi\` gives an OpenAPI 3.1 document per app.
174
+ - **Applied migrations are never reformatted or edited** (D-0145): their contents are recorded when they run. Keep code formatters (prettier, biome, eslint --fix) out of the migrations folder; \`npx dokebi add\` writes the ignore entry for prettier and biome, and \`npx dokebi check\` warns (\`formatter-migrations\`) when a formatter would still format it. The same goes for \`src/dokebi.generated.ts\`, which the CLI writes and refuses once it changed (\`formatter-generated\`, doctor \`MODIFIED_REGISTRATION\`). Change the schema with a new migration; never pass \`--accept-changed\` to make a run pass.
113
175
 
114
176
  ## 3. Before you say it is done
115
177
 
116
- - \`npx dokebi check --json\`: the code convention check (empty \`catch\` and \`.catch(() => {})\`, a promise without await, syntax Node cannot strip (enum, parameter properties, namespaces with values, decorators), private paths such as \`@dokebi/*/dist\` and \`@dokebi/*\` packages that are not installed, edited generated files, secrets that would be committed). Every problem has \`file\`, \`line\` and \`fix\`: fix it as \`fix\` says. Only an empty catch or a background promise that is really intended gets a \`// dokebi-check: allow <rule> <reason>\` comment.
117
- - Say "done" only after \`npx dokebi verify --json\` returns \`ok: true\` (doctor, check, typecheck, build, start, HTTP checks, stop).
118
- - **Your final message to the user must include verify's \`tellUser\` lines**, word for word: the choices of the recipe and who made them (a default with its reason) and what to know, such as no CSRF protection for HTML forms. They come from \`.dokebi/recipe.json\`, which \`recipe\` writes; do not edit it.
178
+ - \`npx dokebi check --json\`: the code convention check (empty \`catch\`, and \`.catch(() => {})\` or \`.catch(() => undefined)\`, a promise without await — also in the \`<script>\` of \`.vue\` files and the \`.js\` under \`views/\` —, syntax Node cannot strip (enum, parameter properties, namespaces with values, decorators), private paths such as \`@dokebi/*/dist\` and \`@dokebi/*\` packages that are not installed, edited generated files, secrets that would be committed; with @dokebi/auth a password compared or hashed by hand (\`plain-password-compare\`), with @dokebi/csrf an EJS post form without the token (\`form-without-csrf\`) and a \`csrf: false\` route that verifies nothing (\`csrf-exempt-unverified\`); with \`src/shared/errors.ts\` an error code written as a string instead of \`errors.CODE()\` (\`unknown-error-code\`)). Every problem has \`file\`, \`line\` and \`fix\`: fix it as \`fix\` says. Only an empty catch or a background promise that is really intended gets a \`// dokebi-check: allow <rule> <reason>\` comment.
179
+ - Say "done" only after \`npx dokebi verify --json\` returns \`ok: true\` (doctor, check, typecheck, build, test, start, HTTP checks, stop).
180
+ - **Tests**: write them with node:test as \`*.test.ts\` files in \`tests/unit\` (no external service) and \`tests/integration\` (a real database, HTTP or broker), with helpers and fixtures in \`tests/support\` (never named \`*.test.ts\`). \`npm test\` runs \`node --test "tests/**/*.test.ts"\`, which covers both test folders; Node strips their types. When the user's design names test cases, start each test name with its id in brackets: \`test('[TC-3] rejects a wrong password', …)\`. verify runs the tests and reports each one (\`tests.results[]\` with \`id\`, \`ok\`, \`error\`); a failing test fails verify. Never delete or skip a failing test to make verify pass: fix the code or the test.
181
+ - **Your final message to the user must include verify's \`tellUser\` lines, copied unchanged** (verify says \`relay: { tellUser: 'verbatim' }\`): do not translate, reword or summarize them, even when you talk to the user in another language; you may add your own summary after them. They say the choices of the recipe and who made them (a default with its reason) and what to know, such as how the forms send the CSRF token and where the admin login and its first password are. They come from \`.dokebi/recipe.json\`, which \`recipe\` writes; do not edit it. Lines that state a fact of the project (whether CSRF, Seal or a session is on, where local mail goes, whether a realm still denies every login, whether a job queue is provisioned) say what verify found when it ran, or that it could not tell and where to look: never restate an older version of one.
119
182
  - When \`ok: false\`, fix the failed items of \`checks[]\` (\`code\`, \`details\`) and run verify again. Never hide a failure or turn a check off.
183
+ - If an installed package's runtime version disagrees with its package.json, report the artifact mismatch and obtain a correctly built package. Never edit node_modules, generated registration metadata or version numbers to make verification pass. Keep application sources, YAML and data while repairing the installation.
184
+ - Verify success covers its checks, not every project requirement. Inspect the final file tree (including ignored configuration files) against the requested layout and configuration policy. Report unresolved doctor warnings and their cause; \`config set\` validates plugin checks and refreshes conditional requirements atomically. After a manual YAML change, set that same option through the CLI to reconcile it; never edit generated metadata. Report any remaining \`STALE_REQUIREMENTS\`.
185
+ - For Inertia, audit shared EJS layouts as well as app/module views for unused scaffolds. Mail EJS templates and a required engine entry are separate resources; preserve them when still used. Remove only scaffolds created for this task after checking registrations and references.
120
186
 
121
187
  ## 4. Structure rules
122
188
 
123
189
  - \`src/main.ts\` is the user entry point, \`dokebi.yaml\` holds the real settings of this machine (kept out of Git), and \`dokebi.example.yaml\` is a sample without secrets.
124
- - Folders (D-0031, D-0032): \`src/apps/<app>\` (one app: app.ts, routes.ts, controllers, models, views), \`src/shared/modules/<module>\` (one feature; create it with \`npx dokebi module add\` and attach it in the app with \`modules: { '/prefix': module }\`), \`src/shared/models\` and \`src/shared/views\` (shared code), \`src/infra/<name>\` (plugin connection code, for example \`src/infra/db/connections.ts\`), \`src/plugins/<name>\` (your own plugins). Business code lives only in apps and shared/modules.
125
- - **Files the CLI rewrites** are only \`src/dokebi.generated.ts\`, \`.dokebi/state.json\` and the part of this file between the \`dokebi:installed\` markers. Never edit them by hand; change them with commands (\`dokebi check\` reports \`generated-file\`).
126
- - **Every other file the CLI or a plugin created is user code**: the shared layout and views (\`src/shared/views/layouts/main.ejs\` and others), entry files (for example \`src/infra/db/connections.ts\`, \`src/infra/web/extension.ts\`), and the app and module files \`app add\` and \`module add\` create (app.ts, routes.ts, controllers, models, views and the examples \`items.ts\`, \`summary.ts\`, \`site.ts\`). **Edit them where they are** (do not move or copy them). doctor does not warn about them, and upgrade does not overwrite an edited one (the new template location is in \`preservedDetails\`).
190
+ - Folders and file names (D-0031, D-0032, D-0092): follow the **Files and folders** rule in \`### Integration rules\` below, which \`npx dokebi guide --sync\` keeps current: apps in \`src/apps/<app>\`, shared code in \`src/shared/{models,services,modules,views,locales,mail}\` and \`src/shared/errors.ts\`, plugin code in \`src/infra/<name>\`, tests in \`tests/unit\`, \`tests/integration\` and \`tests/support\`, tools in \`scripts/\`. App-specific business code lives in apps; reusable features live in shared/modules and cross-app services in shared/services. Shared services must not import app-specific code.
191
+ - App roots contain \`app.ts\` and \`routes.ts\`; module roots contain \`module.ts\` and \`routes.ts\`. Registration files compose dependencies and routes. Use Controller -> Service -> Model for application data. Controllers handle HTTP, services provide data through models and own workflows, models own persistence, validation owns input rules, and jobs invoke services. Services accept explicit inputs and return DTOs without HTTP or template objects. Add these folders only when needed. Route groups may live in \`routes/\` and contain only bindings and access policies. Stateless controllers may be instantiated once by their route group; resource-owning controllers use \`defineController\` and app/module registration.
192
+ - Replace or remove unused scaffold controllers, models and views when implementing the real feature; do not keep an inactive parallel implementation. Inertia pages belong to the mounting app's \`views/<template>/Pages\`, components to its \`components/\`, reusable browser/SSR integration to \`views/runtime/\` (keep the generated \`views/app.js\` and \`views/ssr.js\` entries; moving them also requires updating Vite inputs and manifest/SSR paths), and shared translation data to \`src/shared/locales/<language>/<namespace>.json\`.
193
+ - **CLI-owned generated files** include \`src/dokebi.generated.ts\`, \`.dokebi/state.json\`, \`.dokebi/recipe.json\` and the parts of this file between the \`dokebi:installed\` and \`dokebi:capabilities\` markers. Commands also update YAML and package metadata transactionally; this is not an exhaustive list of files a command may write. Refresh the maintained guidance with \`npx dokebi guide --sync\`. Never edit them by hand; change them with commands (\`dokebi check\` reports \`generated-file\`). The block between the \`dokebi:project\` markers is the project's: the CLI never changes it; add the project's own rules there when the user asks you to record one.
194
+ - **Scaffold source files declared owner: user are user code**: the shared layout and views (\`src/shared/views/layouts/main.ejs\` and others), entry files (for example \`src/infra/db/connections.ts\`, \`src/infra/web/extension.ts\`), and the app and module files \`app add\` and \`module add\` create (app.ts, routes.ts, controllers, models, views and the examples \`items.ts\`, \`summary.ts\`, \`site.ts\`). **Maintain them as user code.** When the user requests structural changes, move them within their app/module and update all imports, resource paths and registration; do not copy them into a second implementation. Never move or hand-edit CLI-owned generated files. doctor does not warn about them, and upgrade does not overwrite an edited one (the new template location is in \`preservedDetails\`).
127
195
  - Node runs the .ts files in \`src\` by stripping their types. Do not use syntax that cannot be stripped, such as constructor parameter properties, enum or namespace (\`npm run typecheck\` reports it). Do not run \`npx tsc\` (the npm package named tsc is not TypeScript).
128
196
  - doctor repairs nothing. Never delete the lock (\`.dokebi/lock\`) or a backup, and never blindly re-run a failed command.
129
197
  - config (\`@dokebi/config\`) is a required part of the core and cannot be removed.
130
198
 
199
+ ### Process topology and background work
200
+
201
+ - Confirm single-process or process-cluster operation and the process count before implementing the runtime. No recipe chooses or installs a process cluster. A generic request does not imply four workers. When the user has already chosen, use that choice. Do not confuse app processes (\`@dokebi/cluster\`), durable job consumers (\`@dokebi/job\`) and CPU threads (\`@dokebi/worker\`).
202
+ - Discover process clustering with \`npx dokebi plugins search process.cluster --json\`. It is a library, not a registered plugin: use the catalog's install command and read \`node_modules/@dokebi/cluster/AI.md\`. There is no blog recipe \`--workers\` flag.
203
+ - In cluster mode, keep \`src/main.ts\` as the parent and put \`createApplication()\` in \`src/infra/runtime/application.ts\`. Store the process count in \`dokebi.yaml\` under \`options.application-runtime.workers\` (integer 1..64), with a non-secret sample. Use ClusterRunner for that project even when the configured count is 1; do not add a second direct-Application branch.
204
+ - The parent resolves \`DOKEBI_CONFIG\` (or the project YAML) to an absolute path and sets it before forking. Each worker loads that same path. YAML is the settings source; no .env is needed, and credentials never cross IPC. A stream name or durable consumer name in YAML is a literal resource name, not an environment variable or the OS worker count.
205
+ - Multi-process state must be shared: sessions, auth throttles and rate-limit counters use Redis; scheduled jobs need distributed coordination; a cluster WebSocket hub is required if the app uses cluster-wide WebSocket/Seal state. Configure only services the app actually uses. Job queues need their named NATS connection, streams and consumers provisioned before modules call \`queue(name)\`. An app process count does not provision a job queue or CPU thread pool.
206
+ - Follow the cluster guide's startup, signals and error handling. Readiness is emitted after all workers are ready. Register SIGINT/SIGTERM before starting, keep handlers through drain, observe stop/closed failures once, and give the runner more time than the app's drain plus cleanup budgets. Verify the chosen count and readiness, same YAML path in workers, cross-worker state where used, and graceful Ctrl+C/SIGTERM. \`dokebi dev\` defaults to one process; use \`DOKEBI_DEV_WORKERS\` explicitly when validating a chosen multi-worker count.
207
+
131
208
  ## 5. Errors and forms (D-0074)
132
209
 
133
- - **Refuse input with \`throw validationError({ field: 'message' })\`** (\`import { validationError } from '@dokebi/web'\`), not with your own \`reply.code(422).send(...)\`. Clients then get exactly what a refused route schema gives them: 422 \`VALIDATION_FAILED\` with \`fields: [{ field, message }]\` and \`errors: { field: message }\`. Other refusals throw a \`DokebiError\` with a \`status\` (403, 404, 409…).
134
- - **JSON or HTML is decided for you**: Inertia requests, \`fetch\`/XHR, Bearer API clients and JSON bodies get JSON errors; only plain browser pages (a navigation, an HTML form post) get an HTML page. Read errors from the JSON (\`body.errors.name\`); do not parse HTML.
210
+ - **Refuse input with \`throw validationError({ field: 'message' })\`** (\`import { validationError } from '@dokebi/web'\`), not with your own \`reply.code(422).send(...)\`. Clients then get exactly what a refused route schema gives them: 422 \`VALIDATION_FAILED\` with \`fields: [{ field, message }]\` and \`errors: { field: message }\`. Other refusals throw a code of the app's error list (next point).
211
+ - **App error codes live in one list** (D-0077): declare them in \`src/shared/errors.ts\` as \`export const errors = defineErrors({ POST_NOT_FOUND: { status: 404, message: 'The post does not exist', hint: 'Check the address.' } })\` (\`import { defineErrors } from '@dokebi/web'\`; a code is upper case with \`_\`, a status 400 to 599, a message one line of English) and throw \`errors.POST_NOT_FOUND({ data: { id } })\`. Add a new code to the list first; never invent one inline (\`new DokebiError('POST_GONE', …)\`, \`reply.code(404).send({ error: 'POST_GONE' })\`): \`npx dokebi check\` warns (\`unknown-error-code\`). Give a route a stable id when the user's design names it: \`config: { id: 'API-12' }\` (one id per route of an app).
212
+ - **JSON or HTML is decided for you**: Inertia requests, \`fetch\`/XHR, Bearer API clients and JSON bodies get JSON errors; only plain browser pages (a navigation, an HTML form post, including one that @dokebi/seal/forms sends sealed with the request header \`x-dokebi-form: 1\`) get an HTML page. Read errors from the JSON (\`body.errors.name\`); do not parse HTML.
135
213
  - **Inertia forms** (\`useForm().post/put/patch/delete\`, \`router.post/…\`): pass \`only\` with the props the submit changes, for example \`form.post(url, { only: ['items'] })\`. With @dokebi/session a validation error goes back (303) with the messages in \`form.errors.<field>\` (\`errors\` always comes along); without \`only\` the GET after the redirect sends every prop of the page again. \`npx dokebi check\` warns about a submit without \`only\` (\`inertia-form-only\`).
136
- - **EJS (HTML) forms come back with their errors**: with @dokebi/session (\`npx dokebi add @dokebi/session\`) an HTML form post refused with 422 goes back (303) to the page it came from, and every EJS page has \`data.errors\` (\`{ field: message }\`), \`data.old\` (the typed values, never passwords) and \`data.flash\` (other flash, for example \`data.flash.notice\`), always objects. Write each field as \`<input name="title" value="<%= data.old.title ?? '' %>">\` with \`<% if (data.errors.title) { %><small><%= data.errors.title %></small><% } %>\`. Do not call \`request.session.takeFlash()\` in a route that renders a page (the page takes the flash). Without a session the refused form shows \`errors/422.ejs\` (else \`errors/error.ejs\`) with \`fields\`, \`errors\` and \`old\`. HTML forms need \`dokebi-web.formBody: true\`.
214
+ - **EJS (HTML) forms come back with their errors**: with @dokebi/session (\`npx dokebi add @dokebi/session\`) an HTML form post refused with 422 goes back (303) to the page it came from, and every EJS page has \`data.errors\` (\`{ field: message }\`), \`data.old\` (the typed values, never passwords) and \`data.flash\` (other flash, for example \`data.flash.notice\`), always objects. Write each field as \`<input name="title" value="<%= data.old.title ?? '' %>">\` with \`<% if (data.errors.title) { %><small><%= data.errors.title %></small><% } %>\`. Do not call \`request.session.takeFlash()\` in a route that renders a page (the page takes the flash). Without a session the refused form shows \`errors/422.ejs\` (else \`errors/error.ejs\`) with \`fields\`, \`errors\` and \`old\`. HTML forms need \`dokebi-web.formBody: true\`, sealed ones too (they are form bodies). On a sealed domain the shared layout writes \`<%- data.seal?.head ?? '' %>\` in its \`<head>\` (first, before an import map of its own): it loads \`@dokebi/seal/forms\`, which sends every post form sealed with no per-form code, and this flow stays the same (see "Required browser transport" below). A form with a chosen file goes as multipart and the module moves \`_csrf\` into the \`x-csrf-token\` header itself. Submit from a script with \`form.requestSubmit()\` or \`submitSealedForm(form)\` (\`@dokebi/seal/forms\`), never \`form.submit()\` (it goes out plain: 415). Never set \`seal: false\` on a form route or mark forms \`data-seal="off"\` to make a plain form post work: a \`data-seal="off"\` form gets 415 \`SEAL_REQUIRED\` on a sealed domain.
215
+ - **Browser files**: an EJS app keeps its style sheets, images, fonts and browser scripts in \`src/apps/<app>/public/\` and links them as \`/assets/<path>\`; with @dokebi/static installed (\`npx dokebi add @dokebi/static\`) @dokebi/app serves that folder on the app hosts (ETag, 304, Range). Never put them in \`views/\` or serve them with a route that reads the file (\`npx dokebi check\` warns \`asset-in-views\` and \`asset-route\`); a font or image a package ships (@dokebi/font-noto-kr) is copied into \`public/\` with its licence file. An Inertia app imports them through Vite.
137
216
  - **Error pages**: an EJS app shows \`views/<template>/errors/<status>.ejs\` (then \`errors/error.ejs\`) for browser pages, with \`status\`, \`error\`, \`message\`, \`hint\`, \`fields\`, \`errors\` and \`old\`; an Inertia app may set \`inertia: { errorPage: 'Error' }\` to render its \`Error\` page component.
138
217
 
139
- ## Installed packages
218
+ ## 6. Login, permissions and CSRF (D-0076)
219
+
220
+ - **Login and permissions use @dokebi/auth** (the blog recipe's \`--admin login\`; ask the user about the admin area like every other choice). It owns no table: its extension class (\`src/infra/auth/extension.ts\`) maps the app's own user table. Sign in with \`await request.auth.attempt({ identity, password })\`, sign out with \`await request.auth.logout()\`, protect routes with \`config: { auth: 'required' }\` or \`config: { can: 'content.write' }\` (the login page \`config: { auth: 'guest' }\`), and put permission rules in \`authorize\`.
221
+ - **Roles on routes** (D-0077 E): when the design has a role table, return \`roles: [row.role]\` from \`credentials\` and declare \`config: { roles: ['admin'] }\` on the route (any one role passes; with \`can\` both must pass) instead of hand-written role \`if\`s in handlers; \`await request.auth.hasRole('admin')\` inside one.
222
+ - **Never compare or hash passwords yourself** (\`===\`, \`timingSafeEqual\`, \`createHash\`, \`scrypt\`, bcrypt): sign-up, password changes and seeds use \`passwords.hash(password)\`, a check uses \`passwords.verify(hash, password)\` (\`npx dokebi check\`: \`plain-password-compare\`). Show **one sentence for every failed login** (never tell an unknown name from a wrong password).
223
+ - **CSRF is @dokebi/csrf** (the blog recipe installs it): every POST, PUT, PATCH and DELETE needs its token. EJS: \`<%- data.csrf.field %>\` inside every \`<form method="post">\` (\`form-without-csrf\` warns); on a sealed domain \`@dokebi/seal/forms\` carries that field inside the sealed body (in the \`x-csrf-token\` header for a multipart form with a file). Inertia navigation/forms: its client supplies the \`X-XSRF-TOKEN\` header. Application API calls use \`sealedFetch\`, which sends the current \`XSRF-TOKEN\` cookie as \`X-XSRF-TOKEN\` on every unsafe same-origin request by itself, multipart uploads included (@dokebi/seal 0.4.2; no adapter of your own).
224
+ - Never turn the CSRF check off, or exempt a route, to make a test or a form pass. Only a webhook that verifies its own signature (an HMAC over the raw body, \`timingSafeEqual\`), an @dokebi/auth Bearer-only route (\`auth: { via: 'token' }\`) or the token refresh route (it calls \`tokens.refresh\`) takes \`config: { csrf: false }\` (\`csrf-exempt-unverified\` warns otherwise). Never change state in a GET route (GET is not checked).
225
+ - Secrets of auth and csrf live in dokebi.yaml (\`dokebi-auth.secrets\`, \`dokebi-csrf.secrets\`, written by \`add\`); the recipe's first admin is \`options.first-admin\` (\`login\`, \`initialPassword\`; a project from an older recipe has \`options.blog-admin\`, which keeps working). Never print the password; tell the user where it is. If you rename the section or sign the admin in by another field (email), change the seed with it: \`npx dokebi verify\` reads both from the project when it tells the user how to sign in.
226
+
227
+ ## 7. Mail (D-0078)
228
+
229
+ - Ask the recipe's development mail question: \`--mail none|mailpit|smtp|ses\`. None installs nothing; Mailpit creates \`compose.mail.yaml\` with a loopback inbox and durable queue. SMTP and SES use settings you supply in \`dokebi.yaml\` (read @dokebi/mail AI.md first; never invent credentials).
230
+ - Templates are user code in \`src/shared/mail/<name>/<locale>/\`, with \`subject.ejs\`, \`html.ejs\` or \`text.ejs\` and optional \`<name>/preview.json\`. See names in \`npx dokebi describe --json\`; render only sample data, without connections or data hooks, with \`npx dokebi mail preview <name> --locale en --json\`.
231
+ - Requests use \`await mail.queue(message)\`, never wait on SMTP; provision @dokebi/job first. Put tokens, passwords and private links in \`sealed\`, not \`data\`. \`dokebi check\` warns with \`mail-send-in-request\` and \`mail-secret-in-data\`.
232
+ - @dokebi/auth supplies password reset and verified signup. The login recipe adds recovery pages when mail is installed; map a real email address in the auth extension. The default admin name is not an email address. Signup pages remain app code; never implement token storage yourself.
233
+
234
+ ${catalog ? `${capabilityBlock(catalog)}\n\n` : ''}## Installed packages
140
235
 
141
236
  ${installedBlock(entries)}
237
+
238
+ ${PROJECT_HEADING}
239
+
240
+ ${projectBlock()}
142
241
  `;
143
242
  }
@@ -35,6 +35,21 @@ export interface Inspection {
35
35
  }>;
36
36
  diagnostics: Diagnostic[];
37
37
  }
38
+ /** Who holds `.dokebi/lock`: the pid the command wrote, whether it still runs, since when. */
39
+ export interface LockHolder {
40
+ readonly pid: number | null;
41
+ /** `kill(pid, 0)`: true alive (EPERM too), false gone (ESRCH), null when it cannot be told. */
42
+ readonly running: boolean | null;
43
+ readonly since: string | null;
44
+ }
45
+ /**
46
+ * Reads `.dokebi/lock` (`{ pid }`, written by Transaction.begin) without taking it (AI-M04 F6):
47
+ * only a signal-0 probe, no `ps`. A reused pid can look alive; `since` lets a reader see an old
48
+ * lock. Null when the lock is gone.
49
+ */
50
+ export declare function lockHolder(root: string): Promise<LockHolder | null>;
51
+ /** The PROJECT_LOCKED diagnostic with what to do (AI-M04 F6). */
52
+ export declare function lockDiagnostic(holder: LockHolder, transactions: readonly string[]): Diagnostic;
38
53
  export declare class ProjectInspector {
39
54
  inspect(directory: string, settings?: {
40
55
  runChecks?: boolean;