@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
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Code formatters and the migrations folder (D-0145). An applied migration must keep its bytes:
|
|
2
|
+
// @dokebi/migrate tells a reformat from a code change by a token checksum, but a project
|
|
3
|
+
// formatter (prettier, biome, eslint --fix with formatting rules) should not touch the folder at
|
|
4
|
+
// all. `dokebi add` keeps the folder out of the formatters it can configure (a CLI-owned entry in
|
|
5
|
+
// .prettierignore, or `!<folder>` in biome.json files.includes), and `dokebi check` warns about a
|
|
6
|
+
// formatter whose config would still format it (rule formatter-migrations).
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { CONFIG_FILE, parseProjectConfig } from './configuration.js';
|
|
10
|
+
import { debug, describeError, isMissing } from './diagnostics.js';
|
|
11
|
+
import { CliError } from './errors.js';
|
|
12
|
+
/** @dokebi/migrate's `directory` default; migrations live in `<directory>/migrations/`. */
|
|
13
|
+
const MIGRATE_DIRECTORY = 'src/infra/db';
|
|
14
|
+
export const PRETTIER_IGNORE = '.prettierignore';
|
|
15
|
+
/** The comment line above the CLI-owned .prettierignore entry. */
|
|
16
|
+
export const PRETTIER_MARKER = '# dokebi: applied migrations are never reformatted (D-0145; @dokebi/migrate checks them)';
|
|
17
|
+
/** The CLI-written registration; the CLI refuses a project whose copy differs (MODIFIED_REGISTRATION). */
|
|
18
|
+
export const GENERATED_FILE = 'src/dokebi.generated.ts';
|
|
19
|
+
/** The comment line above the CLI-owned .prettierignore entry of the generated registration. */
|
|
20
|
+
export const PRETTIER_GENERATED_MARKER = '# dokebi: the CLI writes src/dokebi.generated.ts and refuses a reformatted copy (MODIFIED_REGISTRATION)';
|
|
21
|
+
/** Whether a path is a migrations folder (bare `migrations` patterns keep any of those out). */
|
|
22
|
+
const isMigrations = (path) => path === 'migrations' || path.endsWith('/migrations');
|
|
23
|
+
const PRETTIER_CONFIGS = [
|
|
24
|
+
'.prettierrc',
|
|
25
|
+
'.prettierrc.json',
|
|
26
|
+
'.prettierrc.json5',
|
|
27
|
+
'.prettierrc.yaml',
|
|
28
|
+
'.prettierrc.yml',
|
|
29
|
+
'.prettierrc.toml',
|
|
30
|
+
'.prettierrc.js',
|
|
31
|
+
'.prettierrc.cjs',
|
|
32
|
+
'.prettierrc.mjs',
|
|
33
|
+
'.prettierrc.ts',
|
|
34
|
+
'.prettierrc.cts',
|
|
35
|
+
'.prettierrc.mts',
|
|
36
|
+
'prettier.config.js',
|
|
37
|
+
'prettier.config.cjs',
|
|
38
|
+
'prettier.config.mjs',
|
|
39
|
+
'prettier.config.ts',
|
|
40
|
+
'prettier.config.cts',
|
|
41
|
+
'prettier.config.mts',
|
|
42
|
+
];
|
|
43
|
+
const BIOME_CONFIGS = ['biome.json', 'biome.jsonc'];
|
|
44
|
+
const ESLINT_CONFIGS = [
|
|
45
|
+
'eslint.config.js',
|
|
46
|
+
'eslint.config.mjs',
|
|
47
|
+
'eslint.config.cjs',
|
|
48
|
+
'eslint.config.ts',
|
|
49
|
+
'eslint.config.mts',
|
|
50
|
+
'eslint.config.cts',
|
|
51
|
+
'.eslintrc',
|
|
52
|
+
'.eslintrc.json',
|
|
53
|
+
'.eslintrc.js',
|
|
54
|
+
'.eslintrc.cjs',
|
|
55
|
+
'.eslintrc.yaml',
|
|
56
|
+
'.eslintrc.yml',
|
|
57
|
+
];
|
|
58
|
+
/** ESLint formats only through formatting rules: eslint-plugin-prettier or @stylistic. */
|
|
59
|
+
const ESLINT_FORMATS = /prettier|@stylistic/;
|
|
60
|
+
async function readText(root, path) {
|
|
61
|
+
try {
|
|
62
|
+
return await readFile(join(root, path), 'utf8');
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (isMissing(error))
|
|
66
|
+
return undefined;
|
|
67
|
+
throw new CliError('READ_FAILED', `Cannot read ${path}`, { cause: error });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The migrations folder of the project: `options.dokebi-migrate.directory` + `/migrations`. */
|
|
71
|
+
export async function migrationsFolder(root) {
|
|
72
|
+
const source = await readText(root, CONFIG_FILE);
|
|
73
|
+
let directory = MIGRATE_DIRECTORY;
|
|
74
|
+
if (source !== undefined) {
|
|
75
|
+
let options = {};
|
|
76
|
+
try {
|
|
77
|
+
options = parseProjectConfig(source).options;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// doctor reports an invalid dokebi.yaml; the folder falls back to the default here.
|
|
81
|
+
debug('migrations folder: dokebi.yaml not readable, default used', {
|
|
82
|
+
error: describeError(error),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const migrate = options['dokebi-migrate'];
|
|
86
|
+
if (typeof migrate?.directory === 'string' && migrate.directory !== '')
|
|
87
|
+
directory = migrate.directory;
|
|
88
|
+
}
|
|
89
|
+
return `${directory.replace(/^\.\//, '').replace(/\/+$/, '')}/migrations`;
|
|
90
|
+
}
|
|
91
|
+
/** A gitignore-style pattern without a leading `/` or a trailing `/`, `/**` or `/*`. */
|
|
92
|
+
function plainPattern(line) {
|
|
93
|
+
return line
|
|
94
|
+
.trim()
|
|
95
|
+
.replace(/^\//, '')
|
|
96
|
+
.replace(/\/\*\*?$/, '')
|
|
97
|
+
.replace(/\/$/, '');
|
|
98
|
+
}
|
|
99
|
+
/** Whether a .prettierignore (gitignore syntax) keeps `folder` out. */
|
|
100
|
+
export function prettierCovers(ignore, folder) {
|
|
101
|
+
// gitignore: the last matching line wins, so a later `!src/infra/db/migrations/` brings the
|
|
102
|
+
// folder back (it used to count as covered because a line before it ignored it).
|
|
103
|
+
let covered = false;
|
|
104
|
+
for (const raw of ignore.split(/\r?\n/)) {
|
|
105
|
+
const line = raw.trim();
|
|
106
|
+
if (line === '' || line.startsWith('#'))
|
|
107
|
+
continue;
|
|
108
|
+
const negated = line.startsWith('!');
|
|
109
|
+
const pattern = plainPattern(negated ? line.slice(1) : line);
|
|
110
|
+
if (pattern === folder ||
|
|
111
|
+
folder.startsWith(`${pattern}/`) ||
|
|
112
|
+
(isMigrations(folder) && (pattern === 'migrations' || pattern === '**/migrations')))
|
|
113
|
+
covered = !negated;
|
|
114
|
+
}
|
|
115
|
+
return covered;
|
|
116
|
+
}
|
|
117
|
+
/** .prettierignore with the CLI-owned entry for `folder` (unchanged when it is covered). */
|
|
118
|
+
export function prettierIgnoreText(ignore, folder) {
|
|
119
|
+
if (prettierCovers(ignore, folder))
|
|
120
|
+
return ignore;
|
|
121
|
+
const end = ignore === '' || ignore.endsWith('\n') ? '' : '\n';
|
|
122
|
+
const file = folder === GENERATED_FILE;
|
|
123
|
+
return `${ignore}${end}${ignore === '' ? '' : '\n'}${file ? PRETTIER_GENERATED_MARKER : PRETTIER_MARKER}\n${folder}${file ? '' : '/'}\n`;
|
|
124
|
+
}
|
|
125
|
+
/** A negated biome pattern that keeps `folder` out (`!src/infra/db/migrations`, or any migrations folder). */
|
|
126
|
+
function biomeExcludes(pattern, folder) {
|
|
127
|
+
if (typeof pattern !== 'string' || !pattern.startsWith('!'))
|
|
128
|
+
return false;
|
|
129
|
+
const plain = plainPattern(pattern.replace(/^!+/, ''));
|
|
130
|
+
return (plain === folder ||
|
|
131
|
+
folder.startsWith(`${plain}/`) ||
|
|
132
|
+
(isMigrations(folder) && plain === '**/migrations'));
|
|
133
|
+
}
|
|
134
|
+
/** Whether a parsed biome config keeps `folder` out (v2 `files.includes`, v1 `files.ignore`). */
|
|
135
|
+
export function biomeCovers(config, folder) {
|
|
136
|
+
const files = config?.files;
|
|
137
|
+
if (Array.isArray(files?.includes) && files.includes.some((item) => biomeExcludes(item, folder)))
|
|
138
|
+
return true;
|
|
139
|
+
return (Array.isArray(files?.ignore) &&
|
|
140
|
+
files.ignore.some((item) => {
|
|
141
|
+
if (typeof item !== 'string')
|
|
142
|
+
return false;
|
|
143
|
+
const plain = plainPattern(item);
|
|
144
|
+
return (plain === folder ||
|
|
145
|
+
folder.startsWith(`${plain}/`) ||
|
|
146
|
+
(isMigrations(folder) && plain === '**/migrations'));
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
function spans(text) {
|
|
150
|
+
const found = new Map();
|
|
151
|
+
let i = 0;
|
|
152
|
+
const space = () => {
|
|
153
|
+
while (i < text.length && /\s/.test(text[i]))
|
|
154
|
+
i++;
|
|
155
|
+
};
|
|
156
|
+
const string = () => {
|
|
157
|
+
const start = i;
|
|
158
|
+
i++;
|
|
159
|
+
while (i < text.length && text[i] !== '"')
|
|
160
|
+
i += text[i] === '\\' ? 2 : 1;
|
|
161
|
+
i++;
|
|
162
|
+
return JSON.parse(text.slice(start, i));
|
|
163
|
+
};
|
|
164
|
+
const value = (path) => {
|
|
165
|
+
space();
|
|
166
|
+
const start = i;
|
|
167
|
+
const char = text[i];
|
|
168
|
+
if (char === '{') {
|
|
169
|
+
i++;
|
|
170
|
+
space();
|
|
171
|
+
while (text[i] !== '}') {
|
|
172
|
+
space();
|
|
173
|
+
const key = string();
|
|
174
|
+
space();
|
|
175
|
+
i++; // :
|
|
176
|
+
value(path ? `${path}.${key}` : key);
|
|
177
|
+
space();
|
|
178
|
+
if (text[i] === ',')
|
|
179
|
+
i++;
|
|
180
|
+
space();
|
|
181
|
+
}
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
else if (char === '[') {
|
|
185
|
+
i++;
|
|
186
|
+
space();
|
|
187
|
+
let index = 0;
|
|
188
|
+
while (text[i] !== ']') {
|
|
189
|
+
value(`${path}[${index++}]`);
|
|
190
|
+
space();
|
|
191
|
+
if (text[i] === ',')
|
|
192
|
+
i++;
|
|
193
|
+
space();
|
|
194
|
+
}
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
else if (char === '"')
|
|
198
|
+
string();
|
|
199
|
+
else
|
|
200
|
+
while (i < text.length && !/[\s,}\]]/.test(text[i]))
|
|
201
|
+
i++;
|
|
202
|
+
found.set(path, { start, end: i });
|
|
203
|
+
};
|
|
204
|
+
value('');
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
/** The indentation of the first indented line (biome's default is a tab). */
|
|
208
|
+
function indentOf(text) {
|
|
209
|
+
return /\n([ \t]+)\S/.exec(text)?.[1] ?? '\t';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* biome.json with `folder` kept out, edited in place so the file stays as biome formats it:
|
|
213
|
+
* `!<folder>` appended to `files.includes` (v2), or `<folder>` to `files.ignore` (v1, `v1`), or
|
|
214
|
+
* a new `"files": { "includes": ["**", "!<folder>"] }` first in the object. Undefined when the
|
|
215
|
+
* text is not plain JSON (biome.jsonc, comments): the caller warns instead.
|
|
216
|
+
*/
|
|
217
|
+
export function biomeConfigText(text, folder, v1 = false) {
|
|
218
|
+
let config;
|
|
219
|
+
try {
|
|
220
|
+
config = JSON.parse(text);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
debug('biome config is not plain JSON; not edited', { error: describeError(error) });
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config))
|
|
227
|
+
return undefined;
|
|
228
|
+
if (biomeCovers(config, folder))
|
|
229
|
+
return text;
|
|
230
|
+
const files = config.files;
|
|
231
|
+
const key = Array.isArray(files?.ignore) || (v1 && !Array.isArray(files?.includes)) ? 'ignore' : 'includes';
|
|
232
|
+
const entry = JSON.stringify(key === 'ignore' ? folder : `!${folder}`);
|
|
233
|
+
const map = spans(text);
|
|
234
|
+
const list = map.get(`files.${key}`);
|
|
235
|
+
if (list) {
|
|
236
|
+
const body = text.slice(list.start + 1, list.end - 1);
|
|
237
|
+
const close = list.end - 1;
|
|
238
|
+
if (body.includes('\n')) {
|
|
239
|
+
// A list over several lines: a new last line with the indentation of the others.
|
|
240
|
+
const last = text.lastIndexOf('\n', close);
|
|
241
|
+
const itemIndent = /\n([ \t]*)\S/.exec(body)?.[1] ?? indentOf(text);
|
|
242
|
+
const before = text.slice(0, last).replace(/\s*$/, '');
|
|
243
|
+
return `${before},\n${itemIndent}${entry}${text.slice(last)}`;
|
|
244
|
+
}
|
|
245
|
+
const inner = body.trim();
|
|
246
|
+
return `${text.slice(0, list.start + 1)}${inner === '' ? entry : `${inner}, ${entry}`}${text.slice(close)}`;
|
|
247
|
+
}
|
|
248
|
+
const indent = indentOf(text);
|
|
249
|
+
const filesSpan = map.get('files');
|
|
250
|
+
if (filesSpan && files && typeof files === 'object') {
|
|
251
|
+
const open = filesSpan.start + 1;
|
|
252
|
+
const multiLine = text.slice(filesSpan.start, filesSpan.end).includes('\n');
|
|
253
|
+
const value = key === 'ignore' ? `[${entry}]` : `["**", ${entry}]`;
|
|
254
|
+
const empty = Object.keys(files).length === 0;
|
|
255
|
+
return multiLine
|
|
256
|
+
? `${text.slice(0, open)}\n${indent}${indent}"${key}": ${value}${empty ? '' : ','}${text.slice(open)}`
|
|
257
|
+
: `${text.slice(0, open)} "${key}": ${value}${empty ? ' ' : ','}${text.slice(open)}`;
|
|
258
|
+
}
|
|
259
|
+
const open = text.indexOf('{') + 1;
|
|
260
|
+
const value = key === 'ignore' ? `{ "ignore": [${entry}] }` : `{ "includes": ["**", ${entry}] }`;
|
|
261
|
+
const empty = Object.keys(config).length === 0;
|
|
262
|
+
return `${text.slice(0, open)}\n${indent}"files": ${value}${empty ? '\n' : ','}${text.slice(open)}`;
|
|
263
|
+
}
|
|
264
|
+
/** The major version of @biomejs/biome the project asks for, when package.json names one. */
|
|
265
|
+
export function biomeMajor(pkg) {
|
|
266
|
+
for (const section of ['devDependencies', 'dependencies']) {
|
|
267
|
+
const range = pkg[section]?.['@biomejs/biome'];
|
|
268
|
+
const major = typeof range === 'string' ? /(\d+)\./.exec(range)?.[1] : undefined;
|
|
269
|
+
if (major !== undefined)
|
|
270
|
+
return Number(major);
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/** The formatters the project configures and whether each leaves `folder` alone. */
|
|
275
|
+
export async function formatterUses(root, folder) {
|
|
276
|
+
const uses = [];
|
|
277
|
+
const pkgText = await readText(root, 'package.json');
|
|
278
|
+
let pkg = {};
|
|
279
|
+
if (pkgText !== undefined)
|
|
280
|
+
try {
|
|
281
|
+
pkg = JSON.parse(pkgText);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw new CliError('INVALID_PACKAGE', 'package.json is not valid JSON', { cause: error });
|
|
285
|
+
}
|
|
286
|
+
const dependsOn = (name) => ['dependencies', 'devDependencies'].some((section) => pkg[section]?.[name] !== undefined);
|
|
287
|
+
let prettier;
|
|
288
|
+
for (const file of PRETTIER_CONFIGS)
|
|
289
|
+
if ((await readText(root, file)) !== undefined) {
|
|
290
|
+
prettier = file;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
if (prettier === undefined && (pkg.prettier !== undefined || dependsOn('prettier')))
|
|
294
|
+
prettier = 'package.json';
|
|
295
|
+
if (prettier !== undefined)
|
|
296
|
+
uses.push({
|
|
297
|
+
tool: 'prettier',
|
|
298
|
+
config: prettier,
|
|
299
|
+
covered: prettierCovers((await readText(root, PRETTIER_IGNORE)) ?? '', folder),
|
|
300
|
+
});
|
|
301
|
+
for (const file of BIOME_CONFIGS) {
|
|
302
|
+
const text = await readText(root, file);
|
|
303
|
+
if (text === undefined)
|
|
304
|
+
continue;
|
|
305
|
+
let config;
|
|
306
|
+
try {
|
|
307
|
+
config = JSON.parse(text);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
// biome.jsonc or comments: read as not covered unless the folder is named as excluded.
|
|
311
|
+
debug('biome config read as text', { file, error: describeError(error) });
|
|
312
|
+
config = text.includes(`!${folder}`) ? { files: { includes: [`!${folder}`] } } : {};
|
|
313
|
+
}
|
|
314
|
+
uses.push({ tool: 'biome', config: file, covered: biomeCovers(config, folder) });
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
for (const file of ESLINT_CONFIGS) {
|
|
318
|
+
const text = await readText(root, file);
|
|
319
|
+
if (text === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
if (ESLINT_FORMATS.test(text))
|
|
322
|
+
uses.push({
|
|
323
|
+
tool: 'eslint',
|
|
324
|
+
config: file,
|
|
325
|
+
covered: text.includes(folder) || (isMigrations(folder) && /['"`]\*\*\/migrations/.test(text)),
|
|
326
|
+
});
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
return uses;
|
|
330
|
+
}
|
|
331
|
+
/** The one-line fix for a formatter that would format the migrations folder. */
|
|
332
|
+
export function formatterFix(use, folder) {
|
|
333
|
+
const file = folder === GENERATED_FILE;
|
|
334
|
+
switch (use.tool) {
|
|
335
|
+
case 'prettier':
|
|
336
|
+
return `add ${folder}${file ? '' : '/'} to ${PRETTIER_IGNORE} (npx dokebi add writes it when it installs a plugin)`;
|
|
337
|
+
case 'biome':
|
|
338
|
+
return `add "!${folder}" to files.includes of ${use.config} (biome 1: "${folder}" to files.ignore)`;
|
|
339
|
+
case 'eslint':
|
|
340
|
+
return file
|
|
341
|
+
? `add "${folder}" to the ignores of ${use.config}: its formatting rules would rewrite the registration the CLI writes`
|
|
342
|
+
: `add "${folder}/**" to the ignores of ${use.config}: its formatting rules would rewrite applied migrations`;
|
|
343
|
+
}
|
|
344
|
+
}
|
package/dist/generator.d.ts
CHANGED
|
@@ -12,13 +12,47 @@ import type { Installation } from './project-state.js';
|
|
|
12
12
|
*
|
|
13
13
|
* The CLI-managed units reach the plugin whose manifest declares their role (D-0070
|
|
14
14
|
* `dokebi.hosts`): `apps` (`dokebi app add`, src/apps/<name>/app.ts) and `modules`
|
|
15
|
-
* (`dokebi module add`, src/shared/modules/<name>/module.ts).
|
|
15
|
+
* (`dokebi module add`, src/shared/modules/<name>/module.ts). The `logger` host (D-0151) gives
|
|
16
|
+
* the exported `createLogger` (src/main.ts passes its logger to the Application, so it writes
|
|
17
|
+
* from the first setup record), receives that logger as `entry.logger` and its options from
|
|
18
|
+
* `options.logging`; without one, `createLogger` returns undefined (core's console logger).
|
|
16
19
|
*/
|
|
17
|
-
export declare function generateRegistration(plugins: PluginManifest[], installations?: Readonly<Record<string, Installation>>, apps?: readonly string[], modules?: readonly string[]): string;
|
|
20
|
+
export declare function generateRegistration(plugins: PluginManifest[], installations?: Readonly<Record<string, Installation>>, apps?: readonly string[], modules?: readonly string[], settings?: readonly string[]): string;
|
|
18
21
|
export declare const LEGACY_MAIN_SOURCE = "import { Application } from '@dokebi/core';\nimport { registerPlugins } from './dokebi.generated.ts';\n\nconst app = new Application();\nregisterPlugins(app);\nawait app.start();\ntry {\n app.logger.log('info', 'Dokebi application started');\n // Run local work here. Long-running apps own their signal and shutdown policy.\n} finally {\n await app.stop();\n}\n";
|
|
19
22
|
/** The 0.2.0 YAML entry point before it became long-running; migrate still recognizes it. */
|
|
20
23
|
export declare const MAIN_SOURCE_V1 = "import { Application, loadConfig } from '@dokebi/core';\nimport { registerPlugins } from './dokebi.generated.ts';\n\nconst document = await loadConfig(new URL('../dokebi.yaml', import.meta.url));\nconst app = new Application({ config: document.options });\nawait registerPlugins(app);\nawait app.start();\ntry {\n app.logger.log('info', 'Dokebi application started');\n // Run local work here. Long-running apps own their signal and shutdown policy.\n} finally {\n await app.stop();\n}\n";
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The long-running entry point before D-0151 (core's console logger only). `add` of a logger
|
|
26
|
+
* plugin rewrites an unchanged copy to MAIN_SOURCE; migrate still recognizes it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MAIN_SOURCE_V2 = "import { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Application, loadConfig } from '@dokebi/core';\nimport { registerPlugins } from './dokebi.generated.ts';\n\nconst configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));\nprocess.env.DOKEBI_CONFIG = configPath;\nconst document = await loadConfig(configPath);\nconst app = new Application({ config: document.options });\nawait registerPlugins(app);\napp.logger.log('info', 'Dokebi application started');\n\n// The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run\n// (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.\nlet stopping: Promise<void> | undefined;\nconst stop = (reason: string) =>\n (stopping ??= app.stop().then(\n () => app.logger.log('info', 'Dokebi application stopped', { reason }),\n () => {\n process.exitCode = 1;\n app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });\n },\n ));\nprocess.once('SIGINT', () => void stop('SIGINT'));\nprocess.once('SIGTERM', () => void stop('SIGTERM'));\nprocess.once('beforeExit', () => void stop('idle'));\n";
|
|
29
|
+
/**
|
|
30
|
+
* The entry point of D-0151 before D-0154 (no configFile: the app could not read dokebi.yaml
|
|
31
|
+
* again). `add` of a logger plugin rewrites an unchanged copy to MAIN_SOURCE; migrate still
|
|
32
|
+
* recognizes it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAIN_SOURCE_V3 = "import { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Application, loadConfig } from '@dokebi/core';\nimport { createLogger, registerPlugins } from './dokebi.generated.ts';\n\nconst configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));\nprocess.env.DOKEBI_CONFIG = configPath;\nconst document = await loadConfig(configPath);\n// The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.\nconst app = new Application({ config: document.options, logger: createLogger(document.options) });\nawait registerPlugins(app);\nawait app.start();\napp.logger.log('info', 'Dokebi application started');\n\n// The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run\n// (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.\nlet stopping: Promise<void> | undefined;\nconst stop = (reason: string) =>\n (stopping ??= app.stop().then(\n () => app.logger.log('info', 'Dokebi application stopped', { reason }),\n () => {\n process.exitCode = 1;\n app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });\n },\n ));\nprocess.once('SIGINT', () => void stop('SIGINT'));\nprocess.once('SIGTERM', () => void stop('SIGTERM'));\nprocess.once('beforeExit', () => void stop('idle'));\n";
|
|
35
|
+
/**
|
|
36
|
+
* The entry point of D-0154 before the IPC start message (AI-M43): `start` knew the app was up
|
|
37
|
+
* only from the info line, which logging.level warn hides. migrate still recognizes it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MAIN_SOURCE_V4 = "import { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Application, loadConfig } from '@dokebi/core';\nimport { createLogger, registerPlugins } from './dokebi.generated.ts';\n\nconst configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));\nprocess.env.DOKEBI_CONFIG = configPath;\nconst document = await loadConfig(configPath);\n// The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.\n// configFile: the app reads dokebi.yaml again when it changes (options marked reloadable apply live).\nconst app = new Application({\n config: document.options,\n configFile: configPath,\n logger: createLogger(document.options),\n});\nawait registerPlugins(app);\nawait app.start();\napp.logger.log('info', 'Dokebi application started');\n\n// The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run\n// (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.\nlet stopping: Promise<void> | undefined;\nconst stop = (reason: string) =>\n (stopping ??= app.stop().then(\n () => app.logger.log('info', 'Dokebi application stopped', { reason }),\n () => {\n process.exitCode = 1;\n app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });\n },\n ));\nprocess.once('SIGINT', () => void stop('SIGINT'));\nprocess.once('SIGTERM', () => void stop('SIGTERM'));\nprocess.once('beforeExit', () => void stop('idle'));\n";
|
|
40
|
+
/**
|
|
41
|
+
* The entry point of the IPC start message before an empty DOKEBI_CONFIG counted as unset
|
|
42
|
+
* (R4CB-1: `DOKEBI_CONFIG= node src/main.ts` resolved '' to the working folder and failed with
|
|
43
|
+
* CONFIG_READ_FAILED, while every CLI command reads dokebi.yaml then, D-0154 D4). migrate still
|
|
44
|
+
* recognizes it; doctor names a main.ts with this line (TEMPLATE_FIX_MISSING, template-fixes.ts).
|
|
45
|
+
*/
|
|
46
|
+
export declare const MAIN_SOURCE_V5 = "import { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Application, loadConfig } from '@dokebi/core';\nimport { createLogger, registerPlugins } from './dokebi.generated.ts';\n\nconst configPath = resolve(process.env.DOKEBI_CONFIG ?? fileURLToPath(new URL('../dokebi.yaml', import.meta.url)));\nprocess.env.DOKEBI_CONFIG = configPath;\nconst document = await loadConfig(configPath);\n// The app logger: @dokebi/logger (options.logging targets) when installed, else core's console logger.\n// configFile: the app reads dokebi.yaml again when it changes (options marked reloadable apply live).\nconst app = new Application({\n config: document.options,\n configFile: configPath,\n logger: createLogger(document.options),\n});\nawait registerPlugins(app);\nawait app.start();\napp.logger.log('info', 'Dokebi application started');\n// dokebi start and verify learn that the app is up from this message as well, so a\n// logging.level above info (which hides the line above) does not look like a failed start.\nif (process.send) {\n process.send({ dokebi: 'started' });\n process.channel?.unref();\n}\n\n// The app runs until SIGINT/SIGTERM (servers, consumers) or until nothing is left to run\n// (one-off work), then drains and cleans up once. A second Ctrl+C exits immediately.\nlet stopping: Promise<void> | undefined;\nconst stop = (reason: string) =>\n (stopping ??= app.stop().then(\n () => app.logger.log('info', 'Dokebi application stopped', { reason }),\n () => {\n process.exitCode = 1;\n app.logger.log('error', 'Dokebi application did not stop cleanly', { reason });\n },\n ));\nprocess.once('SIGINT', () => void stop('SIGINT'));\nprocess.once('SIGTERM', () => void stop('SIGTERM'));\nprocess.once('beforeExit', () => void stop('idle'));\n";
|
|
47
|
+
/**
|
|
48
|
+
* src/main.ts of a new project. D-0151: the logger comes from the generated registration
|
|
49
|
+
* (@dokebi/logger when installed, else undefined: core's console logger), so it is in place
|
|
50
|
+
* before the first plugin logs. D-0154: configFile lets the app read dokebi.yaml again while it
|
|
51
|
+
* runs (a change of the file, SIGHUP, npx dokebi reload); options marked reloadable apply
|
|
52
|
+
* without a restart. An empty DOKEBI_CONFIG means dokebi.yaml, as for every CLI command (D-0154
|
|
53
|
+
* D4; `??` took '' for a path and read the working folder).
|
|
54
|
+
*/
|
|
55
|
+
export declare const MAIN_SOURCE: string;
|
|
22
56
|
/**
|
|
23
57
|
* tsconfig.json written by init. `node src/main.ts` strips types without compiling, so the
|
|
24
58
|
* editor and `tsc` should reject syntax Node cannot run (erasableSyntaxOnly) and need explicit
|
|
@@ -33,4 +67,12 @@ export declare const MAIN_SOURCE = "import { Application, loadConfig } from '@do
|
|
|
33
67
|
export declare const PROJECT_DEV_DEPENDENCIES: Readonly<Record<string, string>>;
|
|
34
68
|
/** `npm run typecheck`: the checks of the generated tsconfig.json (it already sets noEmit). */
|
|
35
69
|
export declare const TYPECHECK_SCRIPT = "tsc -p tsconfig.json --noEmit";
|
|
70
|
+
/**
|
|
71
|
+
* `npm test` (D-0077 H): node:test over the project's `*.test.ts` files under tests (Node strips
|
|
72
|
+
* their types; none exist at first, which passes). `dokebi verify` runs it with a result per
|
|
73
|
+
* test. The pattern is quoted so node expands it the same way on every shell. It covers the
|
|
74
|
+
* maintained test folders tests/unit and tests/integration; helpers in tests/support are not
|
|
75
|
+
* named *.test.ts (D-0092 §3, the "Files and folders" rule of capability-guide.ts).
|
|
76
|
+
*/
|
|
77
|
+
export declare const TEST_SCRIPT = "node --test \"tests/**/*.test.ts\"";
|
|
36
78
|
export declare const TSCONFIG_SOURCE: string;
|