@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
package/dist/generator.js CHANGED
@@ -1,4 +1,4 @@
1
- import { hostOf } from './manifest.js';
1
+ import { findHost, hostOf } from './manifest.js';
2
2
  import { CLI_VERSION } from './output.js';
3
3
  /** Import specifier of a user entry file, relative to src/dokebi.generated.ts. */
4
4
  function entrySpecifier(target) {
@@ -16,45 +16,64 @@ function entrySpecifier(target) {
16
16
  *
17
17
  * The CLI-managed units reach the plugin whose manifest declares their role (D-0070
18
18
  * `dokebi.hosts`): `apps` (`dokebi app add`, src/apps/<name>/app.ts) and `modules`
19
- * (`dokebi module add`, src/shared/modules/<name>/module.ts).
19
+ * (`dokebi module add`, src/shared/modules/<name>/module.ts). The `logger` host (D-0151) gives
20
+ * the exported `createLogger` (src/main.ts passes its logger to the Application, so it writes
21
+ * from the first setup record), receives that logger as `entry.logger` and its options from
22
+ * `options.logging`; without one, `createLogger` returns undefined (core's console logger).
20
23
  */
21
- export function generateRegistration(plugins, installations = {}, apps = [], modules = []) {
24
+ export function generateRegistration(plugins, installations = {}, apps = [], modules = [], settings = []) {
22
25
  for (const [role, units] of [
23
26
  ['apps', apps],
24
27
  ['modules', modules],
25
28
  ])
26
29
  if (units.length)
27
30
  hostOf(plugins, role);
31
+ const logger = findHost(plugins, 'logger');
28
32
  const imports = [];
29
33
  const factories = [];
30
34
  /** Plugins the CLI passes an entry (an entry file or the units they host). */
31
35
  const withEntry = new Set();
32
36
  plugins.forEach((plugin, index) => {
33
- imports.push(`import { createPlugin as factory${index} } from ${JSON.stringify(plugin.packageName)};`);
34
37
  const entry = installations[plugin.id]?.entry;
38
+ if (plugin === logger) {
39
+ imports.push(`import { createLogger as loggerFactory, createPlugin as factory${index} } from ${JSON.stringify(plugin.packageName)};`);
40
+ factories.push(` factory${index}({ app: app.view, options: optionsOf('logging'), entry: appLogger === undefined ? undefined : { logger: appLogger } }),`);
41
+ withEntry.add(plugin.id);
42
+ return;
43
+ }
44
+ imports.push(`import { createPlugin as factory${index} } from ${JSON.stringify(plugin.packageName)};`);
35
45
  if (entry) {
36
46
  imports.push(`import * as entry${index} from ${JSON.stringify(entrySpecifier(entry))};`);
37
47
  withEntry.add(plugin.id);
38
48
  }
39
- let entryCode = entry ? `, entry: entry${index}` : '';
49
+ // The units of the roles the plugin hosts, together in one entry: a plugin hosting apps and
50
+ // modules (or apps with an entry file of its own) used to get only the last of them.
51
+ const units = [];
40
52
  // The apps host receives the apps recorded by `dokebi app add` (src/apps/<name>/app.ts).
41
53
  if (plugin.hosts?.includes('apps')) {
42
54
  apps.forEach((name, appIndex) => {
43
55
  imports.push(`import app${appIndex} from ${JSON.stringify(`./apps/${name}/app.ts`)};`);
44
56
  });
45
- entryCode = `, entry: { apps: { ${apps.map((name, appIndex) => `${JSON.stringify(name)}: app${appIndex}`).join(', ')} } }`;
46
- withEntry.add(plugin.id);
57
+ units.push(`apps: { ${apps.map((name, appIndex) => `${JSON.stringify(name)}: app${appIndex}`).join(', ')} }`);
47
58
  }
48
59
  // The modules host receives the modules recorded by `dokebi module add` (D-0032), after the
49
60
  // list of an older entry file (src/shared/modules/index.ts) when the project still has one.
50
- if (plugin.hosts?.includes('modules') && modules.length) {
61
+ const hostsModules = plugin.hosts?.includes('modules') === true && modules.length > 0;
62
+ if (hostsModules) {
51
63
  modules.forEach((name, moduleIndex) => {
52
64
  imports.push(`import module${moduleIndex} from ${JSON.stringify(`./shared/modules/${name}/module.ts`)};`);
53
65
  });
54
66
  const list = modules.map((_name, moduleIndex) => `module${moduleIndex}`);
55
- entryCode = `, entry: { modules: [${[...(entry ? [`...entry${index}.modules`] : []), ...list].join(', ')}] }`;
56
- withEntry.add(plugin.id);
67
+ units.push(`modules: [${[...(entry ? [`...entry${index}.modules`] : []), ...list].join(', ')}]`);
57
68
  }
69
+ const own = entry && !hostsModules ? `...entry${index}, ` : '';
70
+ const entryCode = units.length
71
+ ? `, entry: { ${own}${units.join(', ')} }`
72
+ : entry
73
+ ? `, entry: entry${index}`
74
+ : '';
75
+ if (units.length)
76
+ withEntry.add(plugin.id);
58
77
  factories.push(` factory${index}({ app: app.view, options: optionsOf(${JSON.stringify(plugin.id)})${entryCode} }),`);
59
78
  });
60
79
  // Plugins with an entry may receive user-chosen dependencies (for example web requires),
@@ -65,8 +84,35 @@ export function generateRegistration(plugins, installations = {}, apps = [], mod
65
84
  dependencies: plugin.dependencies.map((dependency) => dependency.name),
66
85
  entry: withEntry.has(plugin.id),
67
86
  }));
87
+ const loggerCode = logger
88
+ ? `let appLogger: ReturnType<typeof loggerFactory> | undefined;
89
+
90
+ /**
91
+ * The app logger of ${logger.packageName} (D-0151) from options.logging: src/main.ts passes it to
92
+ * new Application({ logger }), so it writes from the first setup record. Created once.
93
+ */
94
+ export function createLogger(options: Readonly<Record<string, unknown>>): Logger | undefined {
95
+ appLogger ??= loggerFactory({ options });
96
+ return appLogger;
97
+ }`
98
+ : `/**
99
+ * No logger plugin is installed (D-0151): undefined, so the Application uses core's console
100
+ * logger. npx dokebi add @dokebi/logger makes this return its logger.
101
+ */
102
+ export function createLogger(_options: Readonly<Record<string, unknown>>): Logger | undefined {
103
+ return undefined;
104
+ }`;
105
+ // D-0155: the settings sections of the application, checked at start like plugin options.
106
+ settings.forEach((section, index) => {
107
+ imports.push(`import settings${index} from ${JSON.stringify(`./shared/settings/${section}.ts`)};`);
108
+ });
109
+ const settingsCode = settings.length
110
+ ? ` // The settings of the application (src/shared/settings): options.<section>, checked here.
111
+ ${settings.map((_section, index) => ` app.register(settings${index}.plugin({ app: app.view }));`).join('\n')}
112
+ `
113
+ : '';
68
114
  return `// Generated by dokebi. Change dokebi.yaml options or plugin entry files instead of this file.
69
- import type { Application, Plugin } from '@dokebi/core';
115
+ import type { Application, Logger, Plugin } from '@dokebi/core';
70
116
  ${imports.join('\n')}
71
117
 
72
118
  const MANIFESTS: readonly {
@@ -76,13 +122,15 @@ const MANIFESTS: readonly {
76
122
  entry: boolean;
77
123
  }[] = ${JSON.stringify(manifests)};
78
124
 
125
+ ${loggerCode}
126
+
79
127
  /**
80
128
  * Creates and registers the installed plugins. Each factory checks its options.<plugin> section
81
129
  * of dokebi.yaml with the plugin's own declaration; an invalid value fails here with
82
130
  * INVALID_PLUGIN_OPTIONS (the plugin, the option paths and a fix).
83
131
  */
84
132
  export async function registerPlugins(app: Application): Promise<void> {
85
- const options: Record<string, unknown> = app.config;
133
+ ${settingsCode} const options: Record<string, unknown> = app.config;
86
134
  const optionsOf = (id: string): unknown =>
87
135
  Object.hasOwn(options, id) && options[id] !== null ? options[id] : {};
88
136
  const plugins: Plugin<unknown>[] = [
@@ -116,12 +164,53 @@ try {
116
164
  await app.stop();
117
165
  }
118
166
  `;
119
- export const MAIN_SOURCE = `import { Application, loadConfig } from '@dokebi/core';
167
+ /**
168
+ * The long-running entry point before D-0151 (core's console logger only). `add` of a logger
169
+ * plugin rewrites an unchanged copy to MAIN_SOURCE; migrate still recognizes it.
170
+ */
171
+ export const MAIN_SOURCE_V2 = `import { resolve } from 'node:path';
172
+ import { fileURLToPath } from 'node:url';
173
+ import { Application, loadConfig } from '@dokebi/core';
120
174
  import { registerPlugins } from './dokebi.generated.ts';
121
175
 
122
- const document = await loadConfig(new URL('../dokebi.yaml', import.meta.url));
176
+ const configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));
177
+ process.env.DOKEBI_CONFIG = configPath;
178
+ const document = await loadConfig(configPath);
123
179
  const app = new Application({ config: document.options });
124
180
  await registerPlugins(app);
181
+ app.logger.log('info', 'Dokebi application started');
182
+
183
+ // The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run
184
+ // (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.
185
+ let stopping: Promise<void> | undefined;
186
+ const stop = (reason: string) =>
187
+ (stopping ??= app.stop().then(
188
+ () => app.logger.log('info', 'Dokebi application stopped', { reason }),
189
+ () => {
190
+ process.exitCode = 1;
191
+ app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });
192
+ },
193
+ ));
194
+ process.once('SIGINT', () => void stop('SIGINT'));
195
+ process.once('SIGTERM', () => void stop('SIGTERM'));
196
+ process.once('beforeExit', () => void stop('idle'));
197
+ `;
198
+ /**
199
+ * The entry point of D-0151 before D-0154 (no configFile: the app could not read dokebi.yaml
200
+ * again). `add` of a logger plugin rewrites an unchanged copy to MAIN_SOURCE; migrate still
201
+ * recognizes it.
202
+ */
203
+ export const MAIN_SOURCE_V3 = `import { resolve } from 'node:path';
204
+ import { fileURLToPath } from 'node:url';
205
+ import { Application, loadConfig } from '@dokebi/core';
206
+ import { createLogger, registerPlugins } from './dokebi.generated.ts';
207
+
208
+ const configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));
209
+ process.env.DOKEBI_CONFIG = configPath;
210
+ const document = await loadConfig(configPath);
211
+ // The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.
212
+ const app = new Application({ config: document.options, logger: createLogger(document.options) });
213
+ await registerPlugins(app);
125
214
  await app.start();
126
215
  app.logger.log('info', 'Dokebi application started');
127
216
 
@@ -140,6 +229,99 @@ process.once('SIGINT', () => void stop('SIGINT'));
140
229
  process.once('SIGTERM', () => void stop('SIGTERM'));
141
230
  process.once('beforeExit', () => void stop('idle'));
142
231
  `;
232
+ /**
233
+ * The entry point of D-0154 before the IPC start message (AI-M43): `start` knew the app was up
234
+ * only from the info line, which logging.level warn hides. migrate still recognizes it.
235
+ */
236
+ export const MAIN_SOURCE_V4 = `import { resolve } from 'node:path';
237
+ import { fileURLToPath } from 'node:url';
238
+ import { Application, loadConfig } from '@dokebi/core';
239
+ import { createLogger, registerPlugins } from './dokebi.generated.ts';
240
+
241
+ const configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));
242
+ process.env.DOKEBI_CONFIG = configPath;
243
+ const document = await loadConfig(configPath);
244
+ // The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.
245
+ // configFile: the app reads dokebi.yaml again when it changes (options marked reloadable apply live).
246
+ const app = new Application({
247
+ config: document.options,
248
+ configFile: configPath,
249
+ logger: createLogger(document.options),
250
+ });
251
+ await registerPlugins(app);
252
+ await app.start();
253
+ app.logger.log('info', 'Dokebi application started');
254
+
255
+ // The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run
256
+ // (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.
257
+ let stopping: Promise<void> | undefined;
258
+ const stop = (reason: string) =>
259
+ (stopping ??= app.stop().then(
260
+ () => app.logger.log('info', 'Dokebi application stopped', { reason }),
261
+ () => {
262
+ process.exitCode = 1;
263
+ app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });
264
+ },
265
+ ));
266
+ process.once('SIGINT', () => void stop('SIGINT'));
267
+ process.once('SIGTERM', () => void stop('SIGTERM'));
268
+ process.once('beforeExit', () => void stop('idle'));
269
+ `;
270
+ /**
271
+ * The entry point of the IPC start message before an empty DOKEBI_CONFIG counted as unset
272
+ * (R4CB-1: `DOKEBI_CONFIG= node src/main.ts` resolved '' to the working folder and failed with
273
+ * CONFIG_READ_FAILED, while every CLI command reads dokebi.yaml then, D-0154 D4). migrate still
274
+ * recognizes it; doctor names a main.ts with this line (TEMPLATE_FIX_MISSING, template-fixes.ts).
275
+ */
276
+ export const MAIN_SOURCE_V5 = `import { resolve } from 'node:path';
277
+ import { fileURLToPath } from 'node:url';
278
+ import { Application, loadConfig } from '@dokebi/core';
279
+ import { createLogger, registerPlugins } from './dokebi.generated.ts';
280
+
281
+ const configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));
282
+ process.env.DOKEBI_CONFIG = configPath;
283
+ const document = await loadConfig(configPath);
284
+ // The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.
285
+ // configFile: the app reads dokebi.yaml again when it changes (options marked reloadable apply live).
286
+ const app = new Application({
287
+ config: document.options,
288
+ configFile: configPath,
289
+ logger: createLogger(document.options),
290
+ });
291
+ await registerPlugins(app);
292
+ await app.start();
293
+ app.logger.log('info', 'Dokebi application started');
294
+ // dokebi start and verify learn that the app is up from this message as well, so a
295
+ // logging.level above info (which hides the line above) does not look like a failed start.
296
+ if (process.send) {
297
+ process.send({ dokebi: 'started' });
298
+ process.channel?.unref();
299
+ }
300
+
301
+ // The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run
302
+ // (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.
303
+ let stopping: Promise<void> | undefined;
304
+ const stop = (reason: string) =>
305
+ (stopping ??= app.stop().then(
306
+ () => app.logger.log('info', 'Dokebi application stopped', { reason }),
307
+ () => {
308
+ process.exitCode = 1;
309
+ app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });
310
+ },
311
+ ));
312
+ process.once('SIGINT', () => void stop('SIGINT'));
313
+ process.once('SIGTERM', () => void stop('SIGTERM'));
314
+ process.once('beforeExit', () => void stop('idle'));
315
+ `;
316
+ /**
317
+ * src/main.ts of a new project. D-0151: the logger comes from the generated registration
318
+ * (@dokebi/logger when installed, else undefined: core's console logger), so it is in place
319
+ * before the first plugin logs. D-0154: configFile lets the app read dokebi.yaml again while it
320
+ * runs (a change of the file, SIGHUP, npx dokebi reload); options marked reloadable apply
321
+ * without a restart. An empty DOKEBI_CONFIG means dokebi.yaml, as for every CLI command (D-0154
322
+ * D4; `??` took '' for a path and read the working folder).
323
+ */
324
+ export const MAIN_SOURCE = MAIN_SOURCE_V5.replace('resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(', 'resolve(process.env.DOKEBI_CONFIG || fileURLToPath(');
143
325
  /**
144
326
  * tsconfig.json written by init. `node src/main.ts` strips types without compiling, so the
145
327
  * editor and `tsc` should reject syntax Node cannot run (erasableSyntaxOnly) and need explicit
@@ -158,6 +340,14 @@ export const PROJECT_DEV_DEPENDENCIES = {
158
340
  };
159
341
  /** `npm run typecheck`: the checks of the generated tsconfig.json (it already sets noEmit). */
160
342
  export const TYPECHECK_SCRIPT = 'tsc -p tsconfig.json --noEmit';
343
+ /**
344
+ * `npm test` (D-0077 H): node:test over the project's `*.test.ts` files under tests (Node strips
345
+ * their types; none exist at first, which passes). `dokebi verify` runs it with a result per
346
+ * test. The pattern is quoted so node expands it the same way on every shell. It covers the
347
+ * maintained test folders tests/unit and tests/integration; helpers in tests/support are not
348
+ * named *.test.ts (D-0092 §3, the "Files and folders" rule of capability-guide.ts).
349
+ */
350
+ export const TEST_SCRIPT = 'node --test "tests/**/*.test.ts"';
161
351
  export const TSCONFIG_SOURCE = `${JSON.stringify({
162
352
  compilerOptions: {
163
353
  target: 'ES2023',
@@ -174,5 +364,5 @@ export const TSCONFIG_SOURCE = `${JSON.stringify({
174
364
  // TypeScript 6+ no longer loads every @types package; init installs @types/node.
175
365
  types: ['node'],
176
366
  },
177
- include: ['src/**/*.ts'],
367
+ include: ['src/**/*.ts', 'tests/**/*.ts'],
178
368
  }, null, 2)}\n`;
package/dist/guide.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface Guide {
23
23
  readonly text: string;
24
24
  }
25
25
  /** The short pointer printed at the top of `dokebi --help`. */
26
- export declare const HELP_POINTER = "AI and automation: read `dokebi guide` (--json) first. Ask the user every choice and end your turn until they answer; finish with `dokebi verify` and say done only when it is ok. Every command takes --json and changing commands take --dry-run. Outside a project run npx -y @dokebi/cli (inside, npx dokebi uses the project copy); the npm package named \"dokebi\" is not this CLI\n";
26
+ export declare const HELP_POINTER: string;
27
27
  export declare const GUIDE_STEPS: readonly GuideStep[];
28
28
  export declare const GUIDE_RULES: readonly string[];
29
29
  export declare function buildGuide(catalog: Catalog): Guide;
package/dist/guide.js CHANGED
@@ -1,7 +1,13 @@
1
- import { COMMANDS } from './commands.js';
1
+ import { ANSWER_SOURCES, ASK_AND_WAIT, BROWSER_APPS, INTEGRATION_RULES, } from './capability-guide.js';
2
+ import { COMMANDS, dryRunList } from './commands.js';
2
3
  import { CLI_VERSION } from './output.js';
3
4
  /** The short pointer printed at the top of `dokebi --help`. */
4
- export const HELP_POINTER = 'AI and automation: read `dokebi guide` (--json) first. Ask the user every choice and end your turn until they answer; finish with `dokebi verify` and say done only when it is ok. Every command takes --json and changing commands take --dry-run. Outside a project run npx -y @dokebi/cli (inside, npx dokebi uses the project copy); the npm package named "dokebi" is not this CLI\n';
5
+ export const HELP_POINTER = `AI and automation: read \`dokebi guide\` (--json) first. Use choices the user already supplied; ask only unresolved recipe choices and wait for those answers; finish with \`dokebi verify\` and say done only when it is ok. Use --json except for interactive dev, logs and start --foreground; only these take --dry-run: ${dryRunList()}. Outside a project run npx -y @dokebi/cli (inside, npx dokebi uses the project copy); the npm package named "dokebi" is not this CLI\n`;
6
+ /**
7
+ * The relay rule of verify's tellUser lines, in the verify step and the rules alike (one text:
8
+ * the two had been written apart and already said it in different words, D-0110 §5).
9
+ */
10
+ const RELAY_TELL_USER = "Your final message to the user must include verify's tellUser lines: copy these lines unchanged (do not translate, reword or summarize them); you may add your own summary after them";
5
11
  export const GUIDE_STEPS = [
6
12
  {
7
13
  command: 'npx -y @dokebi/cli init <folder> && cd <folder>',
@@ -9,47 +15,58 @@ export const GUIDE_STEPS = [
9
15
  },
10
16
  {
11
17
  command: 'npx dokebi recipe list --json',
12
- why: 'Pick the recipe that fits the request (a blog → blog, a JSON API → api). If none fits, use plugins search',
18
+ why: `Pick the recipe that fits the request: a service people use in a browser → site (or blog for posts), an API the user asked for, whose clients are other programs → api (no browser screens). ${BROWSER_APPS} If none fits, use plugins search`,
13
19
  },
14
20
  {
15
21
  command: 'npx dokebi recipe <name> --json',
16
- why: 'Without answers it installs nothing and returns questions[] (waitForUser: true). Show the user every question with its options, pros/cons and default, then **end your turn**. 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.',
22
+ why: `Without answers it installs nothing and returns questions[] (waitForUser: true). Pass the choices the user already gave as flags; show the user every question still in missing[] with its options, pros/cons and default, then **end your turn**. ${ASK_AND_WAIT}`,
17
23
  },
18
24
  {
19
- command: 'npx dokebi recipe <name> --view <v> --db <d> --dev-infra <i> --admin <a> --answers-from user --dry-run --json',
25
+ command: 'npx dokebi recipe <name> --view <v> --db <d> --dev-infra <i> --admin <a> --mail <m> --answers-from user --dry-run --json',
20
26
  why: 'Only after the user answered: see the plan (plan.steps) and the preflight for those answers. Ask the user about a preflight error (never switch the database or a port behind their back)',
21
27
  },
22
28
  {
23
- command: 'npx dokebi recipe <name> --view <v> --db <d> --dev-infra <i> --admin <a> --answers-from user --json',
24
- why: 'Apply the plan. Applying needs the source of the answers: --answers-from user (the user answered) or --defaults (the user said to decide); without one it is ANSWERS_SOURCE_REQUIRED. Tell the user the report of the result. Each step runs an existing transactional command; a failure stops with completedSteps, and the same command resumes',
29
+ command: 'npx dokebi recipe <name> --view <v> --db <d> --dev-infra <i> --admin <a> --mail <m> --answers-from user --json',
30
+ why: `Apply the plan. Applying needs the source of the answers: --answers-from user (the user answered) or --defaults (the user said to decide); without one it is ANSWERS_SOURCE_REQUIRED. ${ANSWER_SOURCES} Tell the user the report of the result. Each step runs an existing transactional command; a failure stops with completedSteps, and the same command resumes`,
25
31
  },
26
32
  {
27
33
  command: 'npx dokebi plugins doc <id> --json',
28
34
  why: 'Read the AI.md of the installed version of every installed plugin (AGENTS.md lists the paths under "Installed packages")',
29
35
  },
36
+ {
37
+ command: 'npx dokebi plugins search process.cluster --json',
38
+ why: 'Before implementing the runtime, confirm whether the user wants a single application process or a process cluster, and the worker count. No recipe configures this. App processes (@dokebi/cluster), durable jobs (@dokebi/job) and CPU threads (@dokebi/worker) are different choices. If cluster is selected, install the library using its catalog install command and read node_modules/@dokebi/cluster/AI.md; the blog recipe has no --workers flag. Follow its complete parent/worker example and shared-state prerequisites',
39
+ },
30
40
  {
31
41
  command: '(write the app code) src/apps/<app>, src/shared/modules/<module>',
32
42
  why: "Follow the nextSteps of the recipe result: attach the module to the app and write the features. Build what Dokebi does not have (absent) in app code, as its instead says. Errors and forms (D-0074): refuse input with throw validationError({ field: 'message' }) from @dokebi/web (422 with errors by field, like a refused schema); Inertia form submits pass only (the props they change) and show form.errors; EJS forms show data.errors.<field> and data.old.<field> (with @dokebi/session a refused form comes back to its page, 303); EJS apps may add views/<template>/errors/404.ejs, errors/422.ejs and errors/error.ejs",
33
43
  },
34
44
  {
35
45
  command: 'npx dokebi check --json',
36
- why: 'Convention check while you write code (empty catch, promise without await, syntax Node cannot strip, private @dokebi paths, missing packages, edited generated files, secrets that would be committed). Every problem has file, line and fix',
46
+ why: 'Convention check while you write code (empty catch, promise without await, syntax Node cannot strip, private @dokebi paths, missing packages, edited generated files, secrets that would be committed; with @dokebi/auth a password compared or hashed by hand, with @dokebi/csrf a post form without the token or a csrf: false route that verifies nothing). Every problem has file, line and fix',
37
47
  },
38
48
  {
39
49
  command: 'npx dokebi verify --json',
40
- why: "doctor, check, typecheck, build, start, HTTP checks and stop. Tell the user it is done only when ok: true. Your final message to the user must include verify's tellUser lines (the recipe's choices with who made them, defaults with their reasons, and notices such as no CSRF protection)",
50
+ why: `doctor, check, typecheck, build, the project's tests (npm test; a result per node:test test, with the [ID] of each name), start, HTTP checks and stop. Tell the user it is done only when ok: true. ${RELAY_TELL_USER}. Those lines are the recipe's choices with who made them, defaults with their reasons, and notices such as how the forms send the CSRF token and where the admin login and its first password are; a notice that states a fact of the project says what verify found when it ran (D-0137)`,
41
51
  },
42
52
  ];
43
53
  export const GUIDE_RULES = [
44
- 'Never decide choices such as the view, the database, the development database or an admin area yourself. Ask the user the questions[] of the recipe and end your turn. Run no recipe, add or config before they answer (never "I will go on with the defaults while you choose")',
45
- 'Use --defaults only when the user left the choice to you ("you decide"), and report the chosen values with their defaultReason afterwards. When the user answered, use --answers-from user',
46
- 'Never invent a plugin, a flag or an API for something Dokebi does not have (absent: auth, csrf, upload, mail, markdown, sqlite, React and more)',
47
- 'Never install or run a package outside @dokebi/ (THIRD_PARTY_PACKAGE) without the permission of the user',
48
- 'Never pass a secret with --set or config set (the shell history keeps it). Write it into dokebi.yaml or use the value dev-infra generated',
49
- 'Preview every changing command with --dry-run --json first. Never blindly re-run a failure, and never delete the lock or a backup',
50
- 'Only src/dokebi.generated.ts, .dokebi/state.json and the part of AGENTS.md between the dokebi:installed markers are rewritten by the CLI: never edit them by hand. Every other file the CLI or a plugin created (the shared layout src/shared/views/layouts/main.ejs, entry files, app and module files) is user code: edit it where it is (do not move it)',
51
- "Never say done before verify returns ok: true. Your final message to the user must include verify's tellUser lines",
52
- 'Errors (D-0074): Inertia, fetch/XHR, Bearer and JSON-body requests get JSON errors and only plain browser pages get HTML, so read errors from the JSON. Refuse input with validationError({ field: message }) (422 VALIDATION_FAILED, fields and errors), never a hand-made 422; give every Inertia form submit an only option; an EJS write form shows data.errors.<field> and data.old.<field> (add @dokebi/session so a refused form comes back to its page)',
54
+ 'Runtime layout: src/main.ts owns process signals; a cluster worker factory belongs in src/infra/runtime/application.ts. Store the selected process count in dokebi.yaml options.application-runtime.workers. Use ClusterRunner even for one worker once the project has chosen cluster mode. Resolve DOKEBI_CONFIG once to an absolute YAML path in the parent and inherit that same path in workers; never send credentials through IPC or introduce .env. Configure shared session/auth/rate-limit stores and scheduler/WebSocket coordination before claiming multiple workers work. Verify the effective worker count, HTTP readiness and graceful group-signal shutdown, not just typecheck',
55
+ 'Use choices already supplied by the user. For unresolved choices such as the view, database, development infrastructure, mail or admin area, ask the applicable questions[] and wait. Run no dependent recipe, add or config before those answers (never "I will go on with the defaults while you choose")',
56
+ `Use --defaults or --ai-chose only when the user left the choice to you ("you decide"), and report the chosen values with their reasons afterwards. When the user answered, use --answers-from user. ${ANSWER_SOURCES}`,
57
+ 'Mail (D-0078): ask --mail none|mailpit|smtp|ses. Mailpit starts local mail and a durable queue; SMTP/SES require existing settings in dokebi.yaml. Inspect templates with describe and render preview.json with npx dokebi mail preview <name> --json. In requests use mail.queue, with tokens and links in sealed, never data',
58
+ 'Never invent a plugin, a flag or an API. Use plugins search and the catalog-derived absent list below; read the installed contract for each supported capability and its limits',
59
+ 'Never install or run a package outside @dokebi/ (THIRD_PARTY_PACKAGE) without the permission of the user; the formatter and the headless browser runner the rules ask for are permitted devDependencies, named in the final report. The tools a dokebi command installs as part of its scaffold (app add --inertia: vite, @vitejs/plugin-vue, vue, @inertiajs/vue3, vue-tsc, @typescript/typescript6) belong to the framework and need no permission',
60
+ "Secrets live in dokebi.yaml (0600, kept out of Git), never in dokebi.example.yaml: add writes the ones a plugin declares (generatedSecrets names them), npx dokebi config secret <id>.<path> generates one later, and a value the user supplies (such as a database URL with a password) is written with your file editor, npx dokebi config set or by the user. An environment variable is only an optional production override where a plugin documents one. dokebi.yaml is the developer's file (D-0165): read and edit it directly, secrets included; npx dokebi config show [<id>[.<path>]] gives it with schema defaults (secrets as <secret: set>, for output you share); to change a secret use npx dokebi config secret <id>.<path> --rotate. What stays out of Git: dokebi.yaml itself and secrets in dokebi.example.yaml",
61
+ 'To see what an app has (routes with their login rule, pages, jobs, schedules, error codes, option keys and environment variable names; never a value), run npx dokebi describe --json: it loads the app without serving or connecting (D-0077). Before you write a migration run npx dokebi describe --with-db --json to see the current tables; npx dokebi describe --openapi gives an OpenAPI 3.1 document per app; npx dokebi drift --expected <file> --json compares the databases (tables, columns, column comments) with a file of the describe shape, such as a design',
62
+ 'Preview commands that support --dry-run with --dry-run --json first; consult command help for the supported flags. Never blindly re-run a failure, and never delete the lock or a backup',
63
+ 'CLI-owned src/dokebi.generated.ts, .dokebi/state.json, .dokebi/recipe.json and the installed/capabilities marker blocks of AGENTS.md must not be edited by hand. Commands also update YAML, package metadata and installation records transactionally; consult command help and ownership metadata before changing a file. The dokebi:project block of AGENTS.md holds the rules of this project: read and follow them; the CLI never changes that block. Scaffold source files declared owner: user (the shared layout src/shared/views/layouts/main.ejs, entry files, app and module files) are user code: maintain it in place unless the user requests restructuring; then update its imports, resources and registration together instead of keeping a duplicate implementation',
64
+ `Never say done before verify returns ok: true. ${RELAY_TELL_USER}`,
65
+ "Login and permissions (D-0076): use @dokebi/auth (the blog recipe's --admin login sets it up; ask the user about the admin area first). Implement its extension class on the app's own user table, sign in with request.auth.attempt({ identity, password }), protect routes with config: { auth: 'required' } or { can: '<ability>' }, and show one sentence for every failed login. Never compare or hash passwords yourself: passwords.hash for sign-up and password changes, passwords.verify to check one",
66
+ "Roles (D-0077): when the design has a role table, return roles: [row.role] from the auth extension's credentials and declare config: { roles: ['admin'] } on the route (any one role passes; with can both must pass) instead of hand-written role ifs in handlers",
67
+ "CSRF (D-0076): @dokebi/csrf checks every POST, PUT, PATCH and DELETE (the blog recipe installs it). EJS: <%- data.csrf.field %> inside every <form method=\"post\"> (on a sealed domain @dokebi/seal/forms, loaded by <%- data.seal?.head ?? '' %> in the layout <head>, carries it inside the sealed body, and in the x-csrf-token header for a multipart form with a file; sealed forms are form bodies, so dokebi-web.formBody: true; a script submits with form.requestSubmit() or submitSealedForm(form), never form.submit(), which goes out plain and gets 415); Inertia navigation/forms: its HTTP client supplies X-XSRF-TOKEN. Application API calls use sealedFetch, which sends the current XSRF-TOKEN cookie as X-XSRF-TOKEN on every unsafe same-origin request by itself (@dokebi/seal 0.4.2; no adapter of your own). Never turn the check off or exempt a route to make a test pass; only a webhook that verifies its own signature, an @dokebi/auth Bearer-only route (auth: { via: 'token' }) or the token refresh route (it calls tokens.refresh) takes config: { csrf: false }",
68
+ 'Errors (D-0074): Inertia, fetch/XHR, Bearer and JSON-body requests get JSON errors and only plain browser pages get HTML (an EJS form post sealed by @dokebi/seal/forms, request header x-dokebi-form: 1, counts as an HTML form post), so read errors from the JSON. Refuse input with validationError({ field: message }) (422 VALIDATION_FAILED, fields and errors), never a hand-made 422; give every Inertia form submit an only option; an EJS write form shows data.errors.<field> and data.old.<field> (add @dokebi/session so a refused form comes back to its page)',
69
+ "App error codes (D-0077): declare them once in src/shared/errors.ts with export const errors = defineErrors({ CODE: { status, message, hint? } }) from @dokebi/web and throw errors.CODE({ data }); add a new code to the list first, never invent one inline (new DokebiError('X', …), send({ error: 'X' }): check warns unknown-error-code). A route the user's design names gets config: { id: '<its id>' }",
53
70
  ];
54
71
  export function buildGuide(catalog) {
55
72
  const recipes = catalog.recipes.map((recipe) => ({
@@ -58,15 +75,19 @@ export function buildGuide(catalog) {
58
75
  questions: recipe.questions,
59
76
  }));
60
77
  const absent = catalog.absent.map((entry) => entry.capability);
78
+ const rules = [
79
+ ...GUIDE_RULES,
80
+ ...INTEGRATION_RULES.filter(Boolean).map((rule) => rule.replace(/^- /, '')),
81
+ ];
61
82
  const text = [
62
83
  `# Dokebi CLI ${CLI_VERSION} — AI first page`,
63
84
  '',
64
- 'When the user asks in one line, such as "make me a blog with the Dokebi framework (@dokebi/cli)", follow these steps in order.',
85
+ 'When the user asks in one line, such as "make me a web service with the Dokebi framework (@dokebi/cli)", follow these steps in order.',
65
86
  '',
66
87
  ...GUIDE_STEPS.map((step, index) => `${index + 1}. ${step.command}\n ${step.why}`),
67
88
  '',
68
89
  '## Rules',
69
- ...GUIDE_RULES.map((rule) => `- ${rule}`),
90
+ ...rules.map((rule) => `- ${rule}`),
70
91
  '',
71
92
  '## Recipes',
72
93
  ...recipes.map((recipe) => `- ${recipe.name}: ${recipe.summary} (questions: ${recipe.questions.join(', ')})`),
@@ -81,7 +102,7 @@ export function buildGuide(catalog) {
81
102
  return {
82
103
  cliVersion: CLI_VERSION,
83
104
  steps: GUIDE_STEPS,
84
- rules: GUIDE_RULES,
105
+ rules,
85
106
  recipes,
86
107
  absent,
87
108
  commands: COMMANDS.map(({ usage, summary }) => ({ usage, summary })),
@@ -1,4 +1,4 @@
1
- export type InstallPhase = 'planInstall' | 'planUpgrade' | 'check';
1
+ export type InstallPhase = 'planInstall' | 'planUpgrade' | 'check' | 'checkOptions';
2
2
  export interface InstallModuleContext {
3
3
  readonly options: Readonly<Record<string, unknown>>;
4
4
  readonly plugins: readonly {
@@ -10,14 +10,40 @@ export interface InstallModuleContext {
10
10
  }
11
11
  export declare class InstallPlanningError extends Error {
12
12
  readonly code: string;
13
- constructor(code: string);
13
+ /**
14
+ * What the module said: the message of the error it threw, or the end of its stderr when the
15
+ * process failed (at most DETAIL_CHARS, `user:password@` and tokens masked). Without it a
16
+ * failing planInstall was only INSTALL_MODULE_FAILED, and its author could not see why.
17
+ */
18
+ readonly detail: string | undefined;
19
+ constructor(code: string, options?: {
20
+ readonly cause?: unknown;
21
+ readonly detail?: string | undefined;
22
+ });
14
23
  }
15
24
  /** Returned when the module does not export the requested phase. */
16
25
  export declare const PHASE_MISSING: unique symbol;
26
+ /**
27
+ * The folders an install module may read (R9): its own package and the packages it depends on
28
+ * (`dependencies`, `optionalDependencies`, `peerDependencies`, transitively), each found the way
29
+ * Node resolves it from the importer's real folder, and allowed both where Node finds it (a
30
+ * `node_modules/<name>` link) and at its real path. Never the project, the workspace root or
31
+ * the rest of `node_modules`, so `.env` files and other projects' files stay unreadable.
32
+ */
33
+ export declare function installReadRoots(packageDirectory: string): Promise<string[]>;
17
34
  /**
18
35
  * Runs one lifecycle phase of a plugin installation module in a child process.
19
36
  * The child uses the Node permission model: it may read only `readRoots`
20
- * (real paths), cannot write files or spawn processes, and receives no
37
+ * (at their real and link paths), cannot write files or spawn processes, and receives no
21
38
  * parent environment variables. Node 24 does not restrict network access.
22
39
  */
40
+ /**
41
+ * The `--allow-fs-read` flags of the restricted child. Node 24.15's permission model denies a
42
+ * granted folder when a sibling folder whose name starts with it is granted too (with
43
+ * `node_modules/@dokebi/web` and `node_modules/@dokebi/websocket`, `realpath` of the first is
44
+ * ERR_ACCESS_DENIED, so `import('@dokebi/web')` fails: seen with D-0162's options check of
45
+ * dokebi-websocket). Such a folder is granted as `<folder>*`, which Node handles; it adds only
46
+ * the siblings that start with the same name.
47
+ */
48
+ export declare function readPermissions(roots: readonly string[]): string[];
23
49
  export declare function runInstallModule(modulePath: string, phase: InstallPhase, context: InstallModuleContext, readRoots?: readonly string[]): Promise<unknown>;