@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.
- package/AI.md +262 -57
- package/README.md +35 -10
- package/catalog.json +2519 -257
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +527 -35
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +2 -0
- package/dist/check/shared-state-rules.js +114 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +307 -31
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +29 -3
- package/dist/install-module.js +142 -13
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +106 -27
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +59 -4
- package/dist/npm-client.js +220 -16
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +206 -20
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +481 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3016 -336
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/server.d.ts +50 -4
- package/dist/server.js +790 -103
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +67 -0
- package/dist/transaction.js +428 -46
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +113 -4
- package/dist/verify.js +326 -28
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
package/dist/env-file.js
CHANGED
|
@@ -51,6 +51,21 @@ export function checkEnvText(text) {
|
|
|
51
51
|
} while (!(lines[index] ?? '').includes(quote));
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The variables of a .env text, checked line by line first (ENV_FILE_INVALID). R4CA-11: a byte
|
|
56
|
+
* order mark (editors on Windows write one) is no part of the first name; util.parseEnv kept it
|
|
57
|
+
* there (`\uFEFFDATABASE_URL`), so the app missed that variable and an install that looked for
|
|
58
|
+
* it appended a second line that won over the user's value.
|
|
59
|
+
*/
|
|
60
|
+
export function parseEnvText(text) {
|
|
61
|
+
let body = text;
|
|
62
|
+
if (body.startsWith('\uFEFF')) {
|
|
63
|
+
debug('The project .env starts with a byte order mark; it is not part of the first name');
|
|
64
|
+
body = body.slice(1);
|
|
65
|
+
}
|
|
66
|
+
checkEnvText(body);
|
|
67
|
+
return parseEnv(body);
|
|
68
|
+
}
|
|
54
69
|
/**
|
|
55
70
|
* The variables of `<root>/.env` (empty when there is none). Throws ENV_FILE_INVALID when the
|
|
56
71
|
* file cannot be read or a line is not `KEY=value`.
|
|
@@ -72,24 +87,67 @@ export function readEnvFile(root) {
|
|
|
72
87
|
},
|
|
73
88
|
});
|
|
74
89
|
}
|
|
75
|
-
|
|
76
|
-
const values = parseEnv(text);
|
|
90
|
+
const values = parseEnvText(text);
|
|
77
91
|
debug('Read the project .env for the app', { file, variables: Object.keys(values) });
|
|
78
92
|
return values;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Variables .env never passes to the app (R4CA-3, user decision 2026-10-26): DOKEBI_CONFIG chooses
|
|
96
|
+
* the settings file, and every CLI command and `npm start` read it from the environment only;
|
|
97
|
+
* from .env it would make only the app the CLI runs read another file than all of them.
|
|
98
|
+
*/
|
|
99
|
+
export const ENVIRONMENT_ONLY = ['DOKEBI_CONFIG'];
|
|
100
|
+
/** What `.env` sets that the app does not get (ENVIRONMENT_ONLY), by line; [] without .env. */
|
|
101
|
+
export function envFileWarnings(root) {
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = readFileSync(join(root, ENV_FILE), 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (isMissing(error))
|
|
108
|
+
return [];
|
|
109
|
+
throw new CliError('ENV_FILE_INVALID', `Unable to read ${ENV_FILE}`, {
|
|
110
|
+
cause: error,
|
|
111
|
+
details: { file: ENV_FILE, fix: `make ${ENV_FILE} a readable file (or remove it)` },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const warnings = [];
|
|
115
|
+
for (const [index, line] of text
|
|
116
|
+
.replace(/^\uFEFF/, '')
|
|
117
|
+
.split(/\r?\n/)
|
|
118
|
+
.entries()) {
|
|
119
|
+
const name = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=/.exec(line)?.[1];
|
|
120
|
+
if (name === undefined || !ENVIRONMENT_ONLY.includes(name))
|
|
121
|
+
continue;
|
|
122
|
+
warnings.push({
|
|
123
|
+
code: 'ENV_DOKEBI_CONFIG_IGNORED',
|
|
124
|
+
path: `${ENV_FILE}:${index + 1}`,
|
|
125
|
+
variable: name,
|
|
126
|
+
hint: `${ENV_FILE} line ${index + 1} sets ${name}, which the app does not get from ${ENV_FILE}: every dokebi command and npm start read the settings file from the environment only. To use another file, set it in the environment (${name}=<file> npx dokebi …), and remove the line`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return warnings;
|
|
130
|
+
}
|
|
80
131
|
/**
|
|
81
132
|
* The environment of an app the CLI runs: the project's .env, then the CLI's own environment on
|
|
82
133
|
* top (it wins), then NODE_ENV (`mode` unless one of them set it) and `extra` (the CLI's own
|
|
83
|
-
* variables such as DOKEBI_DEV or the verify and instance tokens).
|
|
134
|
+
* variables such as DOKEBI_DEV or the verify and instance tokens). ENVIRONMENT_ONLY variables are
|
|
135
|
+
* never taken from .env (envFileWarnings names their lines).
|
|
84
136
|
*/
|
|
85
137
|
export function appEnvironment(root, mode, extra = {}, fileValues = readEnvFile(root)) {
|
|
86
|
-
const
|
|
138
|
+
const passed = Object.fromEntries(Object.entries(fileValues).filter(([key]) => !ENVIRONMENT_ONLY.includes(key)));
|
|
139
|
+
const ignored = Object.keys(fileValues).filter((key) => ENVIRONMENT_ONLY.includes(key));
|
|
140
|
+
if (ignored.length)
|
|
141
|
+
debug('Variables of .env the app does not get (environment only)', { variables: ignored });
|
|
142
|
+
const overridden = Object.keys(passed).filter((key) => process.env[key] !== undefined);
|
|
87
143
|
if (overridden.length)
|
|
88
144
|
debug('Variables of .env already set in the environment; the environment wins', {
|
|
89
145
|
variables: overridden,
|
|
90
146
|
});
|
|
91
|
-
const merged = { ...
|
|
92
|
-
|
|
147
|
+
const merged = { ...passed, ...process.env };
|
|
148
|
+
// D-0161: an app the CLI runs on this machine (start, verify, dev) is no deployment, even under
|
|
149
|
+
// NODE_ENV=production: the plugins keep memory stores for one process there.
|
|
150
|
+
return { ...merged, NODE_ENV: merged.NODE_ENV ?? mode, DOKEBI_LOCAL_RUN: '1', ...extra };
|
|
93
151
|
}
|
|
94
152
|
/** Whether a variable is set in the environment or in the project's .env (values not returned). */
|
|
95
153
|
export function envHas(root, name) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface CliErrorOptions extends ErrorOptions {
|
|
2
2
|
details?: unknown;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The confirmed stage (docs/cli/automation.md): `arguments` (the input was refused), `plan` (the
|
|
5
|
+
* change was being prepared and checked, after the project and any staged install were read
|
|
6
|
+
* and before the commit: nothing in the project was written, D-0116), `install`, `commit`, or
|
|
7
|
+
* `unknown`. A failure in `arguments` or `plan` always has `mutation: none`.
|
|
8
|
+
*/
|
|
9
|
+
phase?: 'arguments' | 'plan' | 'install' | 'commit' | 'unknown';
|
|
4
10
|
}
|
|
5
11
|
export declare class CliError extends Error {
|
|
6
12
|
readonly code: string;
|
package/dist/example-config.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export declare function syncExample(source: string, id: string, update: {
|
|
|
7
7
|
readonly defaults?: readonly Record<string, unknown>[];
|
|
8
8
|
readonly changes?: readonly ConfigChange[];
|
|
9
9
|
readonly schema?: Record<string, unknown> | null;
|
|
10
|
+
/** The logger host was added: the sample gets the targets dokebi.yaml got (AI-M46). */
|
|
11
|
+
readonly loggingTargets?: boolean;
|
|
10
12
|
}): string;
|
|
11
13
|
export declare function markExampleRemoved(source: string, id: string): string;
|
package/dist/example-config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyConfigChanges, markRemoved } from './config-edit.js';
|
|
2
|
+
import { addLoggingTargets } from './configuration.js';
|
|
2
3
|
import { scaffoldConfig } from './configuration-scaffold.js';
|
|
3
4
|
import { mergeInstallDefaults } from './merge-install-defaults.js';
|
|
4
5
|
/**
|
|
@@ -9,6 +10,8 @@ export function syncExample(source, id, update) {
|
|
|
9
10
|
let result = applyConfigChanges(source, id, update.changes ?? []);
|
|
10
11
|
for (const defaults of update.defaults ?? [])
|
|
11
12
|
result = mergeInstallDefaults(result, id, defaults);
|
|
13
|
+
if (update.loggingTargets)
|
|
14
|
+
result = addLoggingTargets(result);
|
|
12
15
|
return scaffoldConfig(result, id, update.schema ?? null);
|
|
13
16
|
}
|
|
14
17
|
export function markExampleRemoved(source, id) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The login of the first admin as dokebi.yaml configures it (never the password). */
|
|
2
|
+
export interface FirstAdminIdentity {
|
|
3
|
+
/** The key in the section (`login`, `email`, …). */
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly value: string;
|
|
6
|
+
readonly kind: 'login' | 'email';
|
|
7
|
+
}
|
|
8
|
+
/** What the project says about its first admin. */
|
|
9
|
+
export interface FirstAdmin {
|
|
10
|
+
/** The section of `options` that holds `initialPassword`; absent when none could be told. */
|
|
11
|
+
readonly section?: string;
|
|
12
|
+
readonly identity?: FirstAdminIdentity;
|
|
13
|
+
/** The seed that creates the first admin, when there is one. */
|
|
14
|
+
readonly seedFile?: string;
|
|
15
|
+
/** The section the seed reads (`config.options['…']`), when it names one. */
|
|
16
|
+
readonly seedSection?: string;
|
|
17
|
+
/** Every section of `options` that holds `initialPassword`. */
|
|
18
|
+
readonly candidates: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* The settings file these options come from (M50: the one DOKEBI_CONFIG names); absent for
|
|
21
|
+
* dokebi.yaml.
|
|
22
|
+
*/
|
|
23
|
+
readonly settingsFile?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The sections of `options` that hold a non-empty `initialPassword`, in file order. */
|
|
26
|
+
export declare function adminSections(options: Record<string, unknown>): string[];
|
|
27
|
+
/** Every `options['…']` / `options.…` section a source file reads, in order (comments skipped). */
|
|
28
|
+
export declare function sectionsRead(source: string): string[];
|
|
29
|
+
/** What `options` and the seed source say about the first admin (no file access). */
|
|
30
|
+
export declare function describeFirstAdmin(options: Record<string, unknown>, seed: {
|
|
31
|
+
file: string;
|
|
32
|
+
text: string;
|
|
33
|
+
} | undefined): FirstAdmin;
|
|
34
|
+
/**
|
|
35
|
+
* Reads the settings file the app reads (DOKEBI_CONFIG, else dokebi.yaml; M50) and the seed;
|
|
36
|
+
* undefined facts when the settings file cannot be read.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readFirstAdmin(root: string): Promise<FirstAdmin>;
|
|
39
|
+
/**
|
|
40
|
+
* The words after `sign in at <url>/login` in the login notice: the configured login and where
|
|
41
|
+
* the initial password is, or that the first-admin settings were not found and where to look.
|
|
42
|
+
*/
|
|
43
|
+
export declare function firstAdminPhrase(found: FirstAdmin): string;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Where the first admin of `dokebi recipe blog --admin login` comes from (D-0115), read from the
|
|
2
|
+
// project itself instead of stated from the recipe's constants: AI-M25's builder renamed the
|
|
3
|
+
// section and signed the first admin in by email, and verify still told the user "the login name
|
|
4
|
+
// admin … at options.blog-admin.initialPassword". The section is the one that holds
|
|
5
|
+
// `initialPassword` (the one the seed reads when it names one), the login is the login or email
|
|
6
|
+
// key configured there. Nothing found is said as such, with where to look: a value that is not
|
|
7
|
+
// in the project is never stated, and the password itself is never read into a text or a log.
|
|
8
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { parseProjectConfig, readProjectSettings } from './configuration.js';
|
|
11
|
+
import { debug, describeError, isMissing } from './diagnostics.js';
|
|
12
|
+
import { ADMIN_PASSWORD_KEY, ADMIN_SECTION, LEGACY_ADMIN_SECTION, SEED_FILE, } from './recipe-login.js';
|
|
13
|
+
const SEEDS = 'src/infra/db/seeds';
|
|
14
|
+
/** Keys that name the first admin's login (the recipe writes `login`). */
|
|
15
|
+
const LOGIN_KEYS = /^(?:login|login_?name|user_?name|user|account|identity)$/i;
|
|
16
|
+
/** Keys that name an email address (AI-M25: the builder signed the admin in by email). */
|
|
17
|
+
const EMAIL_KEYS = /e-?mail/i;
|
|
18
|
+
/** A value that can be shown on one line (anything else is not repeated). */
|
|
19
|
+
const SHOWN = /^[^\s\p{Cc}]{1,254}$/u;
|
|
20
|
+
function record(value) {
|
|
21
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The sections of `options` that hold a non-empty `initialPassword`, in file order. */
|
|
26
|
+
export function adminSections(options) {
|
|
27
|
+
return Object.entries(options).flatMap(([name, value]) => {
|
|
28
|
+
const password = record(value)?.[ADMIN_PASSWORD_KEY];
|
|
29
|
+
return typeof password === 'string' && password.length > 0 ? [name] : [];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Every `options['…']` / `options.…` section a source file reads, in order (comments skipped). */
|
|
33
|
+
export function sectionsRead(source) {
|
|
34
|
+
const found = [];
|
|
35
|
+
const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1');
|
|
36
|
+
const pattern = /\boptions\s*(?:\?\.)?\s*(?:\[\s*(['"`])([^'"`\n]+)\1\s*\]|\.\s*([A-Za-z_$][\w$]*(?:-[\w$]+)*))/g;
|
|
37
|
+
for (const match of code.matchAll(pattern)) {
|
|
38
|
+
const name = match[2] ?? match[3];
|
|
39
|
+
if (name && !found.includes(name))
|
|
40
|
+
found.push(name);
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
/** The login or email key of `section`, preferring one the seed reads. */
|
|
45
|
+
function identityOf(section, seed) {
|
|
46
|
+
const keys = Object.entries(section).flatMap(([key, value]) => key !== ADMIN_PASSWORD_KEY &&
|
|
47
|
+
(LOGIN_KEYS.test(key) || EMAIL_KEYS.test(key)) &&
|
|
48
|
+
typeof value === 'string' &&
|
|
49
|
+
SHOWN.test(value.trim())
|
|
50
|
+
? [{ key, value: value.trim() }]
|
|
51
|
+
: []);
|
|
52
|
+
const read = (key) => {
|
|
53
|
+
const name = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
return new RegExp(`(?:\\.\\s*${name}\\b|\\[\\s*['"\`]${name}['"\`]\\s*\\])`).test(seed);
|
|
55
|
+
};
|
|
56
|
+
const chosen = keys.find((item) => read(item.key)) ?? keys[0];
|
|
57
|
+
if (!chosen)
|
|
58
|
+
return undefined;
|
|
59
|
+
return { ...chosen, kind: EMAIL_KEYS.test(chosen.key) ? 'email' : 'login' };
|
|
60
|
+
}
|
|
61
|
+
/** The seed file of the first admin: the recipe's, else a seed that names `initialPassword`. */
|
|
62
|
+
async function seedSource(root) {
|
|
63
|
+
try {
|
|
64
|
+
return { file: SEED_FILE, text: await readFile(join(root, SEED_FILE), 'utf8') };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!isMissing(error))
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
let connections = [];
|
|
71
|
+
try {
|
|
72
|
+
connections = (await readdir(join(root, SEEDS), { withFileTypes: true }))
|
|
73
|
+
.filter((entry) => entry.isDirectory())
|
|
74
|
+
.map((entry) => entry.name)
|
|
75
|
+
.sort();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (!isMissing(error))
|
|
79
|
+
throw error;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
for (const connection of connections) {
|
|
83
|
+
const names = (await readdir(join(root, SEEDS, connection))).filter((name) => /\.[cm]?ts$/.test(name));
|
|
84
|
+
for (const name of names.sort()) {
|
|
85
|
+
const file = `${SEEDS}/${connection}/${name}`;
|
|
86
|
+
const text = await readFile(join(root, file), 'utf8');
|
|
87
|
+
if (text.includes(ADMIN_PASSWORD_KEY))
|
|
88
|
+
return { file, text };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** What `options` and the seed source say about the first admin (no file access). */
|
|
94
|
+
export function describeFirstAdmin(options, seed) {
|
|
95
|
+
const candidates = adminSections(options);
|
|
96
|
+
const read = seed ? sectionsRead(seed.text) : [];
|
|
97
|
+
// The section the seed reads that holds the password, else the first one it reads (in code
|
|
98
|
+
// order: the read itself comes before any message that names it).
|
|
99
|
+
const seedSection = read.find((name) => candidates.includes(name)) ?? read[0];
|
|
100
|
+
const base = {
|
|
101
|
+
candidates,
|
|
102
|
+
...(seed ? { seedFile: seed.file } : {}),
|
|
103
|
+
...(seedSection ? { seedSection } : {}),
|
|
104
|
+
};
|
|
105
|
+
// The seed decides when it names a section; otherwise the one section holding the password.
|
|
106
|
+
const section = seedSection
|
|
107
|
+
? candidates.includes(seedSection)
|
|
108
|
+
? seedSection
|
|
109
|
+
: undefined
|
|
110
|
+
: candidates.length === 1
|
|
111
|
+
? candidates[0]
|
|
112
|
+
: (candidates.find((name) => name === ADMIN_SECTION) ??
|
|
113
|
+
candidates.find((name) => name === LEGACY_ADMIN_SECTION));
|
|
114
|
+
if (!section)
|
|
115
|
+
return base;
|
|
116
|
+
const identity = identityOf(record(options[section]) ?? {}, seed?.text ?? '');
|
|
117
|
+
return { ...base, section, ...(identity ? { identity } : {}) };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Reads the settings file the app reads (DOKEBI_CONFIG, else dokebi.yaml; M50) and the seed;
|
|
121
|
+
* undefined facts when the settings file cannot be read.
|
|
122
|
+
*/
|
|
123
|
+
export async function readFirstAdmin(root) {
|
|
124
|
+
let options = {};
|
|
125
|
+
let settingsFile;
|
|
126
|
+
try {
|
|
127
|
+
const settings = await readProjectSettings(root);
|
|
128
|
+
if (settings.target.fromEnvironment)
|
|
129
|
+
settingsFile = settings.target.file;
|
|
130
|
+
options = parseProjectConfig(settings.source).options;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
debug('First admin: the settings file not read', { error: describeError(error) });
|
|
134
|
+
}
|
|
135
|
+
let seed;
|
|
136
|
+
try {
|
|
137
|
+
seed = await seedSource(root);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
debug('First admin: seed not read', { error: describeError(error) });
|
|
141
|
+
}
|
|
142
|
+
const found = {
|
|
143
|
+
...describeFirstAdmin(options, seed),
|
|
144
|
+
...(settingsFile ? { settingsFile } : {}),
|
|
145
|
+
};
|
|
146
|
+
debug('First admin settings', {
|
|
147
|
+
section: found.section ?? null,
|
|
148
|
+
identityKey: found.identity?.key ?? null,
|
|
149
|
+
seedFile: found.seedFile ?? null,
|
|
150
|
+
seedSection: found.seedSection ?? null,
|
|
151
|
+
candidates: found.candidates,
|
|
152
|
+
});
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The words after `sign in at <url>/login` in the login notice: the configured login and where
|
|
157
|
+
* the initial password is, or that the first-admin settings were not found and where to look.
|
|
158
|
+
*/
|
|
159
|
+
export function firstAdminPhrase(found) {
|
|
160
|
+
const file = found.settingsFile ?? 'dokebi.yaml';
|
|
161
|
+
const seed = found.seedFile ?? `the seed that creates the first admin (${SEEDS}/<connection>/)`;
|
|
162
|
+
if (!found.section) {
|
|
163
|
+
const why = found.seedSection
|
|
164
|
+
? `${seed} reads options.${found.seedSection}, and ${file} has no ${ADMIN_PASSWORD_KEY} there`
|
|
165
|
+
: found.candidates.length > 1
|
|
166
|
+
? `more than one section under options of ${file} holds ${ADMIN_PASSWORD_KEY} (${found.candidates.join(', ')})`
|
|
167
|
+
: `no section under options of ${file} holds ${ADMIN_PASSWORD_KEY}`;
|
|
168
|
+
return `as the first admin, but the CLI could not find the first-admin settings (${why}: renamed, or removed once the admin existed). Look in ${seed} and the options of ${file} for the login and the initial password, and never print the password`;
|
|
169
|
+
}
|
|
170
|
+
const at = `options.${found.section}`;
|
|
171
|
+
const who = found.identity
|
|
172
|
+
? `the ${found.identity.kind === 'email' ? 'email address' : 'login name'} ${found.identity.value}`
|
|
173
|
+
: `the login ${seed} gives the first admin (${at} of ${file} has no login or email key)`;
|
|
174
|
+
return `with ${who} and the initial password stored in ${file} at ${at}.${ADMIN_PASSWORD_KEY} (it is never printed; change it before going live and then remove the key)`;
|
|
175
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const PRETTIER_IGNORE = ".prettierignore";
|
|
2
|
+
/** The comment line above the CLI-owned .prettierignore entry. */
|
|
3
|
+
export declare const PRETTIER_MARKER = "# dokebi: applied migrations are never reformatted (D-0145; @dokebi/migrate checks them)";
|
|
4
|
+
/** The CLI-written registration; the CLI refuses a project whose copy differs (MODIFIED_REGISTRATION). */
|
|
5
|
+
export declare const GENERATED_FILE = "src/dokebi.generated.ts";
|
|
6
|
+
/** The comment line above the CLI-owned .prettierignore entry of the generated registration. */
|
|
7
|
+
export declare const PRETTIER_GENERATED_MARKER = "# dokebi: the CLI writes src/dokebi.generated.ts and refuses a reformatted copy (MODIFIED_REGISTRATION)";
|
|
8
|
+
export type FormatterTool = 'prettier' | 'biome' | 'eslint';
|
|
9
|
+
/** One formatter the project configures, and whether it leaves the migrations folder alone. */
|
|
10
|
+
export interface FormatterUse {
|
|
11
|
+
readonly tool: FormatterTool;
|
|
12
|
+
/** The config file (`package.json` for a `prettier` key or dependency). */
|
|
13
|
+
readonly config: string;
|
|
14
|
+
/** True when its ignore settings already keep the folder out. */
|
|
15
|
+
readonly covered: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The migrations folder of the project: `options.dokebi-migrate.directory` + `/migrations`. */
|
|
18
|
+
export declare function migrationsFolder(root: string): Promise<string>;
|
|
19
|
+
/** Whether a .prettierignore (gitignore syntax) keeps `folder` out. */
|
|
20
|
+
export declare function prettierCovers(ignore: string, folder: string): boolean;
|
|
21
|
+
/** .prettierignore with the CLI-owned entry for `folder` (unchanged when it is covered). */
|
|
22
|
+
export declare function prettierIgnoreText(ignore: string, folder: string): string;
|
|
23
|
+
/** Whether a parsed biome config keeps `folder` out (v2 `files.includes`, v1 `files.ignore`). */
|
|
24
|
+
export declare function biomeCovers(config: unknown, folder: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* biome.json with `folder` kept out, edited in place so the file stays as biome formats it:
|
|
27
|
+
* `!<folder>` appended to `files.includes` (v2), or `<folder>` to `files.ignore` (v1, `v1`), or
|
|
28
|
+
* a new `"files": { "includes": ["**", "!<folder>"] }` first in the object. Undefined when the
|
|
29
|
+
* text is not plain JSON (biome.jsonc, comments): the caller warns instead.
|
|
30
|
+
*/
|
|
31
|
+
export declare function biomeConfigText(text: string, folder: string, v1?: boolean): string | undefined;
|
|
32
|
+
/** The major version of @biomejs/biome the project asks for, when package.json names one. */
|
|
33
|
+
export declare function biomeMajor(pkg: Record<string, unknown>): number | undefined;
|
|
34
|
+
/** The formatters the project configures and whether each leaves `folder` alone. */
|
|
35
|
+
export declare function formatterUses(root: string, folder: string): Promise<FormatterUse[]>;
|
|
36
|
+
/** The one-line fix for a formatter that would format the migrations folder. */
|
|
37
|
+
export declare function formatterFix(use: FormatterUse, folder: string): string;
|