@dokebi/cli 0.4.2 → 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 +518 -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 +20 -2
  110. package/dist/install-module.js +45 -8
  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 +17 -2
  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 +40 -4
  124. package/dist/npm-client.js +152 -15
  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 +195 -16
  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 +3006 -333
  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 +43 -4
  170. package/dist/server.js +601 -52
  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 +47 -0
  180. package/dist/transaction.js +241 -7
  181. package/dist/verify-tests.d.ts +98 -0
  182. package/dist/verify-tests.js +449 -0
  183. package/dist/verify.d.ts +103 -3
  184. package/dist/verify.js +272 -21
  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
@@ -0,0 +1,48 @@
1
+ import { CliError } from './errors.js';
2
+ export declare const SETTINGS_FOLDER = "src/shared/settings";
3
+ /** Section names: as core's SETTINGS_SECTION (lower-case word, at most 64 characters). */
4
+ export declare const SETTINGS_SECTION: RegExp;
5
+ /** Core's own sections (options.logging, options.reload): never application settings. */
6
+ export declare const RESERVED_SECTIONS: ReadonlySet<string>;
7
+ export declare const settingsFile: (section: string) => string;
8
+ export declare const settingsSchemaFile: (section: string) => string;
9
+ /** The camelCase name of the export a settings file default-exports (`survey-limits` → surveyLimits). */
10
+ export declare function settingsIdentifier(section: string): string;
11
+ /** The file `settings add` writes: one reloadable and one restart-only option to start from. */
12
+ export declare function settingsSource(section: string): string;
13
+ /**
14
+ * The options schema of a settings section from its declaration: the file is imported in a child
15
+ * process in the project (its @dokebi/core), and core renders the schema. The text, as
16
+ * options.schema.json of plugins (two-space JSON, a final newline).
17
+ */
18
+ export declare function renderSettingsSchema(root: string, section: string): Promise<string>;
19
+ /** renderSettingsSchema from a declaration at `path` (a draft beside where it goes). */
20
+ export declare function renderSettingsSchemaFrom(root: string, section: string, path: string): Promise<string>;
21
+ /** The schema of a settings section from its schema file, or undefined when it is missing. */
22
+ export declare function readSettingsSchema(root: string, section: string): Promise<Record<string, unknown> | undefined>;
23
+ /**
24
+ * Sections of dokebi.yaml the framework names itself without a declaration: the cluster's process
25
+ * count (`application-runtime`, read by the generated main and by @dokebi/outbox) and the first
26
+ * admin a recipe writes (`first-admin`, older `blog-admin`). doctor does not report them.
27
+ */
28
+ export declare const CONVENTION_SECTIONS: ReadonlySet<string>;
29
+ /** Why a section of dokebi.yaml has no declaration: one code for config show/set/unset/secret. */
30
+ export interface UndeclaredSection {
31
+ readonly code: 'UNKNOWN_PLUGIN' | 'UNDECLARED_SECTION';
32
+ readonly message: string;
33
+ readonly details: Record<string, unknown> & {
34
+ readonly fix: string;
35
+ };
36
+ }
37
+ /**
38
+ * D-0155: a section that no installed plugin, core section or settings section declares. A
39
+ * plugin id of the catalog (or `logging` without the logger) is a plugin that is not installed
40
+ * (UNKNOWN_PLUGIN, fix: add it); any other name is a section nobody declared
41
+ * (UNDECLARED_SECTION, fix: settings add). config set and secret refuse it (a value cannot be
42
+ * checked); config show and unset work on what is there and warn with the same code.
43
+ */
44
+ export declare function undeclaredSection(section: string, packageOf: (id: string) => string | undefined, installed: readonly string[], settings: readonly string[]): UndeclaredSection;
45
+ /** The refusal of config set or secret for an undeclared section, as a CLI error. */
46
+ export declare function undeclaredSectionError(found: UndeclaredSection): CliError;
47
+ /** The package of a catalog plugin id (`dokebi-redis` → `@dokebi/redis`), exact ids only. */
48
+ export declare function catalogPackageOf(): Promise<(id: string) => string | undefined>;
@@ -0,0 +1,168 @@
1
+ // D-0155: the settings of the application itself in their own section of dokebi.yaml,
2
+ // `options.<section>`, declared in src/shared/settings/<section>.ts with core's defineSettings
3
+ // (a defineOptions declaration). The CLI keeps the section's JSON Schema beside it,
4
+ // src/shared/settings/<section>.schema.json, written from the declaration (`settings add` and
5
+ // `settings sync` import the file in a child process; Node strips the types), so every command
6
+ // reads it without a build: config show/set/unset/secret, doctor (which also says when the file
7
+ // no longer matches the declaration), the example file and the reloadable marks treat the
8
+ // section like a plugin's.
9
+ import { execFile } from 'node:child_process';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { promisify } from 'node:util';
14
+ import { loadSchema } from '@dokebi/config';
15
+ import { loadCatalog } from './catalog.js';
16
+ import { debug } from './diagnostics.js';
17
+ import { CliError } from './errors.js';
18
+ const exec = promisify(execFile);
19
+ export const SETTINGS_FOLDER = 'src/shared/settings';
20
+ /** Section names: as core's SETTINGS_SECTION (lower-case word, at most 64 characters). */
21
+ export const SETTINGS_SECTION = /^[a-z][a-z0-9-]{0,63}$/;
22
+ /** Core's own sections (options.logging, options.reload): never application settings. */
23
+ export const RESERVED_SECTIONS = new Set(['logging', 'reload']);
24
+ export const settingsFile = (section) => `${SETTINGS_FOLDER}/${section}.ts`;
25
+ export const settingsSchemaFile = (section) => `${SETTINGS_FOLDER}/${section}.schema.json`;
26
+ /** The camelCase name of the export a settings file default-exports (`survey-limits` → surveyLimits). */
27
+ export function settingsIdentifier(section) {
28
+ return `${section.replace(/-([a-z0-9])/g, (_match, letter) => letter.toUpperCase())}Settings`;
29
+ }
30
+ /** The file `settings add` writes: one reloadable and one restart-only option to start from. */
31
+ export function settingsSource(section) {
32
+ return `// The settings of this application in dokebi.yaml: options.${section} (D-0155). Every count,
33
+ // size and time is an option with a minimum, a maximum and a default; reloadable ones apply while
34
+ // the app runs (save dokebi.yaml, or npx dokebi reload), the others need a restart.
35
+ // Read them where they are used: ${settingsIdentifier(section)}.read(context.app.config) (typed, cached per
36
+ // configuration). After changing the declaration run npx dokebi settings sync (doctor says when).
37
+ // Keep this file free of side effects: the CLI imports it to write ${section}.schema.json.
38
+ import { defineOptions, defineSettings, option as o } from '@dokebi/core';
39
+
40
+ export default defineSettings({
41
+ section: '${section}',
42
+ options: defineOptions({
43
+ pageSize: o.integer({
44
+ minimum: 1,
45
+ maximum: 1000,
46
+ default: 50,
47
+ reloadable: true,
48
+ description: 'Items per page of a list',
49
+ }),
50
+ workers: o.integer({
51
+ minimum: 1,
52
+ maximum: 64,
53
+ default: 2,
54
+ description: 'Background workers started with the app (a restart applies a change)',
55
+ }),
56
+ }),
57
+ });
58
+ `;
59
+ }
60
+ /**
61
+ * The options schema of a settings section from its declaration: the file is imported in a child
62
+ * process in the project (its @dokebi/core), and core renders the schema. The text, as
63
+ * options.schema.json of plugins (two-space JSON, a final newline).
64
+ */
65
+ export async function renderSettingsSchema(root, section) {
66
+ return renderSettingsSchemaFrom(root, section, join(root, settingsFile(section)));
67
+ }
68
+ /** renderSettingsSchema from a declaration at `path` (a draft beside where it goes). */
69
+ export async function renderSettingsSchemaFrom(root, section, path) {
70
+ const url = pathToFileURL(path).href;
71
+ const script = `
72
+ const { renderOptionsSchema } = await import('@dokebi/core');
73
+ const module = await import(${JSON.stringify(url)});
74
+ const settings = module.default;
75
+ if (!settings || settings.section !== ${JSON.stringify(section)} || !settings.options) {
76
+ process.stderr.write('The default export is not defineSettings({ section: ${JSON.stringify(section)}, options })\\n');
77
+ process.exit(3);
78
+ }
79
+ process.stdout.write(renderOptionsSchema(settings.options));
80
+ `;
81
+ try {
82
+ const { stdout } = await exec(process.execPath, ['--input-type=module', '-e', script], {
83
+ cwd: root,
84
+ timeout: 30_000,
85
+ maxBuffer: 4 * 1024 * 1024,
86
+ env: { ...process.env, NODE_OPTIONS: '' },
87
+ });
88
+ debug('Settings schema rendered from its declaration', { section, bytes: stdout.length });
89
+ return stdout;
90
+ }
91
+ catch (cause) {
92
+ const stderr = String(cause.stderr ?? '').trim();
93
+ throw new CliError('SETTINGS_SCHEMA_FAILED', `The settings declaration ${settingsFile(section)} could not be read`, {
94
+ cause,
95
+ details: {
96
+ section,
97
+ file: settingsFile(section),
98
+ // The first lines of the error the import gave (code, not configuration values).
99
+ error: stderr.split('\n').slice(0, 20).join('\n'),
100
+ fix: `make ${settingsFile(section)} default-export defineSettings({ section: '${section}', options: defineOptions({...}) }) without side effects, then run npx dokebi settings sync`,
101
+ },
102
+ });
103
+ }
104
+ }
105
+ /** The schema of a settings section from its schema file, or undefined when it is missing. */
106
+ export async function readSettingsSchema(root, section) {
107
+ const path = join(root, settingsSchemaFile(section));
108
+ try {
109
+ await readFile(path, 'utf8');
110
+ }
111
+ catch (error) {
112
+ if (error.code === 'ENOENT')
113
+ return undefined;
114
+ throw error;
115
+ }
116
+ return loadSchema(path);
117
+ }
118
+ /**
119
+ * Sections of dokebi.yaml the framework names itself without a declaration: the cluster's process
120
+ * count (`application-runtime`, read by the generated main and by @dokebi/outbox) and the first
121
+ * admin a recipe writes (`first-admin`, older `blog-admin`). doctor does not report them.
122
+ */
123
+ export const CONVENTION_SECTIONS = new Set([
124
+ 'application-runtime',
125
+ 'first-admin',
126
+ 'blog-admin',
127
+ ]);
128
+ /**
129
+ * D-0155: a section that no installed plugin, core section or settings section declares. A
130
+ * plugin id of the catalog (or `logging` without the logger) is a plugin that is not installed
131
+ * (UNKNOWN_PLUGIN, fix: add it); any other name is a section nobody declared
132
+ * (UNDECLARED_SECTION, fix: settings add). config set and secret refuse it (a value cannot be
133
+ * checked); config show and unset work on what is there and warn with the same code.
134
+ */
135
+ export function undeclaredSection(section, packageOf, installed, settings) {
136
+ const pkg = section === 'logging' ? '@dokebi/logger' : packageOf(section);
137
+ if (pkg !== undefined)
138
+ return {
139
+ code: 'UNKNOWN_PLUGIN',
140
+ message: `${section} is not installed`,
141
+ details: {
142
+ plugin: section,
143
+ package: pkg,
144
+ installed: [...installed],
145
+ settings: [...settings],
146
+ fix: `install it first: npx dokebi add ${pkg} (its options are then checked against its schema)`,
147
+ },
148
+ };
149
+ return {
150
+ code: 'UNDECLARED_SECTION',
151
+ message: `options.${section} is not declared: no installed plugin, core section or settings section of the application reads it`,
152
+ details: {
153
+ section,
154
+ installed: [...installed],
155
+ settings: [...settings],
156
+ fix: `declare it: npx dokebi settings add ${section} (it writes ${settingsFile(section)}, or adopts that file when it default-exports defineSettings({ section: '${section}', options })), then read it with ${settingsIdentifier(section)}.read(context.app.config); config set, unset, secret and show then check it like the options of a plugin`,
157
+ },
158
+ };
159
+ }
160
+ /** The refusal of config set or secret for an undeclared section, as a CLI error. */
161
+ export function undeclaredSectionError(found) {
162
+ return new CliError(found.code, found.message, { details: found.details, phase: 'arguments' });
163
+ }
164
+ /** The package of a catalog plugin id (`dokebi-redis` → `@dokebi/redis`), exact ids only. */
165
+ export async function catalogPackageOf() {
166
+ const catalog = await loadCatalog();
167
+ return (id) => catalog.packages.find((item) => item.id === id)?.package;
168
+ }
@@ -0,0 +1,39 @@
1
+ /** The code the scaffold's stub hooks log (written by the site recipe since it exists). */
2
+ export declare const SITE_AUTH_STUB_CODE = "SITE_AUTH_UNCONFIGURED";
3
+ /** How long the whole probe may take (import, every hook); then the child is killed. */
4
+ export declare const USER_STORE_PROBE_MS = 20000;
5
+ /** What one hook did when the probe called it. */
6
+ export interface HookProbe {
7
+ /**
8
+ * `stub`: the scaffold's deny stub; `app`: anything else (reads a store, finds someone, throws,
9
+ * takes time); `unimplemented`: the AuthExtension base answered EXTENSION_REQUIRED.
10
+ */
11
+ readonly state: 'stub' | 'app' | 'unimplemented';
12
+ /** How an `app` hook showed it: `dependency`, `found`, `none`, `threw`, `pending`. */
13
+ readonly how?: string;
14
+ }
15
+ /** One realm of the extension module. */
16
+ export interface RealmProbe {
17
+ readonly realm: string;
18
+ /** False: the module exports no class for this realm. */
19
+ readonly defined: boolean;
20
+ /** Why the realm's class could not be made (its constructor threw). */
21
+ readonly failure?: string;
22
+ readonly findByIdentity?: HookProbe;
23
+ readonly findById?: HookProbe;
24
+ }
25
+ export type UserStoreProbe = {
26
+ readonly ok: true;
27
+ readonly realms: readonly RealmProbe[];
28
+ } | {
29
+ readonly ok: false;
30
+ readonly code: string;
31
+ readonly message: string;
32
+ };
33
+ /** Runs in the project's folder (`node --input-type=module --eval`), like the describe runner. */
34
+ export declare const USER_STORE_RUNNER: string;
35
+ /**
36
+ * Probes the hooks of `realms` in the project's auth extension. Never throws: what went wrong is
37
+ * the result (`ok: false` with a code and a one-line reason) and a debug line with the error.
38
+ */
39
+ export declare function probeUserStores(root: string, realms: readonly string[], timeoutMs?: number): Promise<UserStoreProbe>;
@@ -0,0 +1,195 @@
1
+ // Whether the realms of a site still use the site scaffold's user-store stub (D-0137). The site
2
+ // recipe writes src/infra/auth/extension.ts with findByIdentity and findById that find nobody
3
+ // (every login is denied) and log the code SITE_AUTH_UNCONFIGURED; the builder connects a realm
4
+ // by writing those hooks against its user store. AI-M27: verify kept telling the user "the
5
+ // generated auth extensions deny every login" after the builder had connected them, because the
6
+ // sentence was fixed text.
7
+ //
8
+ // How it is told: by the hooks' behaviour, not by their text. A child process in the project's
9
+ // folder imports the extension module (nothing else of the app: no plugin is set up, nothing
10
+ // listens or connects) and calls each hook of each realm once with a probe identity and a hook
11
+ // context whose getDependency throws (no store can be reached) and whose logger records the codes
12
+ // it is given. A hook is the stub only when it answers undefined, logs SITE_AUTH_UNCONFIGURED and
13
+ // asks for no dependency: an edited hook that reads a store asks for its dependency, finds
14
+ // someone, throws or takes time, and is app code. Projects of every site recipe so far carry the
15
+ // same stub (one version since the recipe exists), so they are told the same way.
16
+ import { spawn } from 'node:child_process';
17
+ import { debug, describeError, maskTexts } from './diagnostics.js';
18
+ import { appEnvironment } from './env-file.js';
19
+ import { exists, securePath } from './filesystem.js';
20
+ import { EXTENSION_FILE } from './recipe-login.js';
21
+ /** The code the scaffold's stub hooks log (written by the site recipe since it exists). */
22
+ export const SITE_AUTH_STUB_CODE = 'SITE_AUTH_UNCONFIGURED';
23
+ /** How long the whole probe may take (import, every hook); then the child is killed. */
24
+ export const USER_STORE_PROBE_MS = 20_000;
25
+ /** How long one hook may take before the probe counts it as app code (the stub answers at once). */
26
+ const HOOK_MS = 2_000;
27
+ /** How long the child may take to exit once it answered. */
28
+ const EXIT_GRACE_MS = 2_000;
29
+ /** Runs in the project's folder (`node --input-type=module --eval`), like the describe runner. */
30
+ export const USER_STORE_RUNNER = `import { pathToFileURL } from 'node:url';
31
+ const base = pathToFileURL(process.cwd() + '/');
32
+ const STUB = ${JSON.stringify(SITE_AUTH_STUB_CODE)};
33
+ const HOOK_MS = ${HOOK_MS};
34
+ const PENDING = Symbol('pending');
35
+ const reason = (error) => {
36
+ let text;
37
+ try { text = error instanceof Error ? error.message : String(error); } catch (failure) { text = typeof error + ' (' + typeof failure + ')'; }
38
+ return (String(text).split('\\n', 1)[0] || 'Unknown error').replace(/\\/\\/[^\\s/]*@/g, '//***@').slice(0, 300);
39
+ };
40
+ const answer = (message) => new Promise((resolve) => process.send(message, () => resolve(undefined)));
41
+ const noDependency = Object.assign(new Error('The CLI user-store probe gives no dependency'), { code: 'CLI_PROBE' });
42
+ async function hook(instance, name, realm) {
43
+ const codes = [];
44
+ let dependency = false;
45
+ const logger = {
46
+ isEnabled: () => true,
47
+ log: (_level, _message, fields) => { if (fields && typeof fields.code === 'string') codes.push(fields.code); },
48
+ };
49
+ logger.child = () => logger;
50
+ const controller = new AbortController();
51
+ const context = Object.freeze({
52
+ realm,
53
+ app: Object.freeze({}),
54
+ logger,
55
+ signal: controller.signal,
56
+ getDependency() { dependency = true; throw noDependency; },
57
+ });
58
+ let timer;
59
+ try {
60
+ const input = name === 'findById' ? 'dokebi-cli-probe' : 'dokebi-cli-probe@example.invalid';
61
+ const value = await Promise.race([
62
+ Promise.resolve().then(() => instance[name](input, context)),
63
+ new Promise((resolve) => { timer = setTimeout(() => resolve(PENDING), HOOK_MS); }),
64
+ ]);
65
+ if (value === PENDING) return { state: 'app', how: 'pending' };
66
+ if (value === undefined && !dependency && codes.includes(STUB)) return { state: 'stub' };
67
+ return { state: 'app', how: dependency ? 'dependency' : value === undefined ? 'none' : 'found' };
68
+ } catch (error) {
69
+ if (error?.code === 'EXTENSION_REQUIRED') return { state: 'unimplemented' };
70
+ return { state: 'app', how: dependency ? 'dependency' : 'threw' };
71
+ } finally {
72
+ clearTimeout(timer);
73
+ controller.abort();
74
+ }
75
+ }
76
+ try {
77
+ const realms = JSON.parse(process.env.DOKEBI_CLI_PROBE_REALMS || '[]');
78
+ const module = await import(new URL(${JSON.stringify(EXTENSION_FILE)}, base).href);
79
+ const classes = new Map();
80
+ if (typeof module.default === 'function') classes.set('default', module.default);
81
+ if (module.realms && typeof module.realms === 'object')
82
+ for (const [name, value] of Object.entries(module.realms)) if (typeof value === 'function') classes.set(name, value);
83
+ const found = [];
84
+ for (const realm of realms) {
85
+ const Realm = classes.get(realm);
86
+ if (!Realm) { found.push({ realm, defined: false }); continue; }
87
+ let instance;
88
+ try { instance = new Realm(); } catch (error) { found.push({ realm, defined: true, failure: reason(error) }); continue; }
89
+ found.push({ realm, defined: true, findByIdentity: await hook(instance, 'findByIdentity', realm), findById: await hook(instance, 'findById', realm) });
90
+ }
91
+ await answer({ type: 'stores', realms: found });
92
+ } catch (error) {
93
+ await answer({ type: 'failure', code: typeof error?.code === 'string' ? error.code : 'EXTENSION_UNREADABLE', message: reason(error) });
94
+ process.exitCode = 1;
95
+ }
96
+ process.disconnect();
97
+ `;
98
+ /**
99
+ * Probes the hooks of `realms` in the project's auth extension. Never throws: what went wrong is
100
+ * the result (`ok: false` with a code and a one-line reason) and a debug line with the error.
101
+ */
102
+ export async function probeUserStores(root, realms, timeoutMs = USER_STORE_PROBE_MS) {
103
+ const started = performance.now();
104
+ try {
105
+ if (!(await exists(await securePath(root, EXTENSION_FILE))))
106
+ return { ok: false, code: 'EXTENSION_MISSING', message: `${EXTENSION_FILE} does not exist` };
107
+ }
108
+ catch (error) {
109
+ debug('User-store probe: the extension path could not be checked', {
110
+ error: describeError(error),
111
+ });
112
+ return {
113
+ ok: false,
114
+ code: 'EXTENSION_UNREADABLE',
115
+ message: `${EXTENSION_FILE} could not be read`,
116
+ };
117
+ }
118
+ let child;
119
+ try {
120
+ child = spawn(process.execPath, ['--input-type=module', '--disable-warning=ExperimentalWarning', '--eval', USER_STORE_RUNNER], {
121
+ cwd: root,
122
+ // The project's .env under the CLI's environment, like dokebi start and describe.
123
+ env: {
124
+ ...appEnvironment(root, 'production'),
125
+ DOKEBI_CLI_PROBE_REALMS: JSON.stringify(realms),
126
+ },
127
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
128
+ });
129
+ }
130
+ catch (error) {
131
+ debug('User-store probe: the child process did not start', { error: describeError(error) });
132
+ return { ok: false, code: 'PROBE_FAILED', message: 'the probe process did not start' };
133
+ }
134
+ // The module's own output: kept (bounded) for the debug line, never printed.
135
+ let output = '';
136
+ const keep = (chunk) => {
137
+ output = (output + chunk.toString('utf8')).slice(-4_096);
138
+ };
139
+ child.stdout?.on('data', keep);
140
+ child.stderr?.on('data', keep);
141
+ const result = await new Promise((resolve) => {
142
+ let message;
143
+ let grace;
144
+ const timer = setTimeout(() => {
145
+ debug('User-store probe: time limit reached; killing it', { timeoutMs });
146
+ child.kill('SIGKILL');
147
+ resolve({
148
+ ok: false,
149
+ code: 'PROBE_TIMEOUT',
150
+ message: `importing ${EXTENSION_FILE} and calling its hooks took more than ${timeoutMs} ms`,
151
+ });
152
+ }, timeoutMs);
153
+ child.on('message', (received) => {
154
+ message = maskTexts(received);
155
+ grace = setTimeout(() => child.kill('SIGKILL'), EXIT_GRACE_MS);
156
+ });
157
+ child.once('error', (error) => {
158
+ clearTimeout(timer);
159
+ if (grace)
160
+ clearTimeout(grace);
161
+ debug('User-store probe: the child process failed', { error: describeError(error) });
162
+ resolve({ ok: false, code: 'PROBE_FAILED', message: error.message });
163
+ });
164
+ child.once('exit', (code, signal) => {
165
+ clearTimeout(timer);
166
+ if (grace)
167
+ clearTimeout(grace);
168
+ if (message?.type === 'stores')
169
+ resolve({ ok: true, realms: message.realms });
170
+ else if (message?.type === 'failure')
171
+ resolve({ ok: false, code: message.code, message: message.message });
172
+ else
173
+ resolve({
174
+ ok: false,
175
+ code: 'PROBE_FAILED',
176
+ message: `the probe process exited (${code ?? signal}) before it answered`,
177
+ });
178
+ });
179
+ });
180
+ debug('User-store probe finished', {
181
+ ok: result.ok,
182
+ ...(result.ok
183
+ ? {
184
+ realms: result.realms.map((item) => ({
185
+ realm: item.realm,
186
+ defined: item.defined,
187
+ findByIdentity: item.findByIdentity?.state ?? null,
188
+ findById: item.findById?.state ?? null,
189
+ })),
190
+ }
191
+ : { code: result.code, reason: result.message, output }),
192
+ ms: Math.round(performance.now() - started),
193
+ });
194
+ return result;
195
+ }
@@ -0,0 +1,26 @@
1
+ import type { Diagnostic } from './project-inspector.js';
2
+ interface TemplateFix {
3
+ readonly id: string;
4
+ readonly marker: RegExp;
5
+ /** What the fix does and the change that brings it over. */
6
+ readonly what: string;
7
+ }
8
+ /** The fixes of each @dokebi/db adapter template (`templates/adapters/<id>.ts`). */
9
+ export declare const ADAPTER_FIXES: Readonly<Record<string, readonly TemplateFix[]>>;
10
+ export interface TemplateSources {
11
+ /** The installed @dokebi/db package name, whose `templates/adapters` the copies came from. */
12
+ readonly db?: string | undefined;
13
+ /** The installed apps host (D-0070), whose `templates/inertia-build` the Vite configs came from. */
14
+ readonly apps?: {
15
+ readonly id: string;
16
+ readonly packageName: string;
17
+ } | undefined;
18
+ /** @dokebi/template-inertia is installed: it serves the build the Vite configs write. */
19
+ readonly inertia: boolean;
20
+ }
21
+ /**
22
+ * TEMPLATE_FIX_MISSING warnings for the copies under `root`. Templates resolve from `base`
23
+ * (the project, or upgrade's staging install) like Node in a workspace member (D-0035).
24
+ */
25
+ export declare function templateFixes(root: string, base: string, sources: TemplateSources): Promise<Diagnostic[]>;
26
+ export {};