@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/describe.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// `dokebi describe [--json]` (D-0077 B): what the app has — routes, pages, jobs, schedules, error
|
|
2
|
+
// codes, option keys and environment variable names — without serving traffic. The project is
|
|
3
|
+
// loaded like `dokebi start` loads it (the settings file, M50; the generated registration, the project's
|
|
4
|
+
// .env under the CLI's environment), in a child process, but in core's describe mode: every
|
|
5
|
+
// plugin's setup runs, no start or ready hook (nothing listens, nothing connects), and the
|
|
6
|
+
// describers plugins contributed are asked. No value of an option or a variable is ever printed.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { redaction } from '@dokebi/core/libraries';
|
|
11
|
+
import { configTarget, readNamedSettings } from './configuration.js';
|
|
12
|
+
import { DB_NOT_READ, describeDatabases } from './describe-db.js';
|
|
13
|
+
import { debug, describeError, maskTexts } from './diagnostics.js';
|
|
14
|
+
import { appEnvironment } from './env-file.js';
|
|
15
|
+
import { CliError } from './errors.js';
|
|
16
|
+
import { checkProject, installedVersion } from './server.js';
|
|
17
|
+
/** The JSON Schema of `result` (shipped next to dist/: packages/cli/describe.schema.json). */
|
|
18
|
+
export const DESCRIBE_SCHEMA_FILE = 'describe.schema.json';
|
|
19
|
+
export const DESCRIBE_SCHEMA_VERSION = 1;
|
|
20
|
+
/** Default limit for loading and describing the app. */
|
|
21
|
+
export const DESCRIBE_TIMEOUT_MS = 30_000;
|
|
22
|
+
/** How long the child may take to exit once it answered. */
|
|
23
|
+
const EXIT_GRACE_MS = 5_000;
|
|
24
|
+
/** The section names that are lists in the result; every other section goes under `app`. */
|
|
25
|
+
const LISTS = new Set(['routes', 'pages', 'jobs', 'schedules', 'env']);
|
|
26
|
+
/**
|
|
27
|
+
* Runs in the project's folder (`node --input-type=module --eval`): bare specifiers resolve from
|
|
28
|
+
* the project's node_modules and relative ones from its root, like src/main.ts.
|
|
29
|
+
*/
|
|
30
|
+
export const DESCRIBE_RUNNER = `import { existsSync } from 'node:fs';
|
|
31
|
+
import { pathToFileURL } from 'node:url';
|
|
32
|
+
const base = pathToFileURL(process.cwd() + '/');
|
|
33
|
+
const reason = (error) => {
|
|
34
|
+
let text;
|
|
35
|
+
try { text = error instanceof Error ? error.message : String(error); } catch { text = typeof error; }
|
|
36
|
+
return (String(text).split('\\n', 1)[0] || 'Unknown error').replace(/\\/\\/[^\\s/]*@/g, '//***@').slice(0, 500);
|
|
37
|
+
};
|
|
38
|
+
const answer = (message) =>
|
|
39
|
+
new Promise((resolve) => process.send(message, () => resolve(undefined)));
|
|
40
|
+
try {
|
|
41
|
+
const { Application, loadConfig, definePoint } = await import('@dokebi/core');
|
|
42
|
+
if (typeof Application.prototype.describe !== 'function') {
|
|
43
|
+
await answer({ type: 'failure', code: 'DESCRIBE_UNSUPPORTED', message: 'The installed @dokebi/core cannot describe an app' });
|
|
44
|
+
} else {
|
|
45
|
+
// M50: the settings file the app reads, resolved by the CLI (DOKEBI_CONFIG, else dokebi.yaml).
|
|
46
|
+
const settings = process.env.DOKEBI_CLI_SETTINGS_FILE;
|
|
47
|
+
const document = await loadConfig(settings ? settings : new URL('dokebi.yaml', base));
|
|
48
|
+
const app = new Application({ config: document.options, runtime: { describing: true } });
|
|
49
|
+
const { registerPlugins } = await import(new URL('src/dokebi.generated.ts', base).href);
|
|
50
|
+
await registerPlugins(app);
|
|
51
|
+
const previewInput = process.env.DOKEBI_CLI_MAIL_PREVIEW;
|
|
52
|
+
if (previewInput) {
|
|
53
|
+
const input = JSON.parse(previewInput);
|
|
54
|
+
const { dependency } = await import('@dokebi/mail');
|
|
55
|
+
const point = definePoint('core:describe');
|
|
56
|
+
app.register({
|
|
57
|
+
metadata: { name: 'cli-mail-preview', version: '1.0.0', core: '*', dependencies: [dependency], contributes: ['core:describe'] },
|
|
58
|
+
setup(context) {
|
|
59
|
+
const mail = context.getDependency(dependency);
|
|
60
|
+
context.logger.log('debug', 'Preparing mail template preview', { template: input.name });
|
|
61
|
+
context.contribute(point, { section: 'mail-preview', describe: async () => {
|
|
62
|
+
if (typeof mail.preview !== 'function') throw Object.assign(new Error('Upgrade @dokebi/mail for template preview'), { code: 'MAIL_PREVIEW_UNSUPPORTED' });
|
|
63
|
+
const result = await mail.preview(input.name, input.locale);
|
|
64
|
+
if (!result) throw Object.assign(new Error('Mail template does not exist'), { code: 'MAIL_TEMPLATE_NOT_FOUND' });
|
|
65
|
+
return result;
|
|
66
|
+
} });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const description = await app.describe();
|
|
71
|
+
let errors = { file: false };
|
|
72
|
+
const file = new URL('src/shared/errors.ts', base);
|
|
73
|
+
if (existsSync(file)) {
|
|
74
|
+
try {
|
|
75
|
+
const module = await import(file.href);
|
|
76
|
+
// errorCatalog of @dokebi/web 0.4.3 (D-0077 D) joins every defineErrors result exported.
|
|
77
|
+
const web = await import('@dokebi/web');
|
|
78
|
+
if (typeof web.errorCatalog !== 'function') throw Object.assign(new Error('The installed @dokebi/web has no errorCatalog (0.4.3 or later)'), { code: 'ERRORS_UNREADABLE' });
|
|
79
|
+
const definitions = (web.errorCatalog(module) ?? []).map((item) => ({
|
|
80
|
+
code: item.code,
|
|
81
|
+
status: item.status,
|
|
82
|
+
message: item.message,
|
|
83
|
+
...(typeof item.hint === 'string' ? { hint: item.hint } : {}),
|
|
84
|
+
}));
|
|
85
|
+
errors = { file: true, definitions };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
errors = { file: true, failure: { code: typeof error?.code === 'string' ? error.code : 'ERRORS_UNREADABLE', reason: reason(error) } };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await answer({ type: 'description', description, errors });
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await answer({
|
|
94
|
+
type: 'failure',
|
|
95
|
+
code: typeof error?.code === 'string' ? error.code : 'DESCRIBE_FAILED',
|
|
96
|
+
message: reason(error),
|
|
97
|
+
...(typeof error?.plugin === 'string' ? { plugin: error.plugin } : {}),
|
|
98
|
+
});
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
process.disconnect();
|
|
102
|
+
`;
|
|
103
|
+
function projectName(root) {
|
|
104
|
+
try {
|
|
105
|
+
const name = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name;
|
|
106
|
+
return typeof name === 'string' ? name : null;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
debug('Unable to read the project name for describe', { error: describeError(error) });
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Loads the project in describe mode in a child process and returns what it said. */
|
|
114
|
+
async function run(root, timeoutMs, mailPreview) {
|
|
115
|
+
// M50: the app loads the file DOKEBI_CONFIG names (else dokebi.yaml), and so does describe; a
|
|
116
|
+
// named file that cannot be read is READ_FAILED, never dokebi.yaml instead.
|
|
117
|
+
const settings = await configTarget(root);
|
|
118
|
+
if (settings.fromEnvironment)
|
|
119
|
+
await readNamedSettings(root, settings);
|
|
120
|
+
debug('Describe loads the settings file', { file: settings.file });
|
|
121
|
+
const child = spawn(process.execPath, ['--input-type=module', '--disable-warning=ExperimentalWarning', '--eval', DESCRIBE_RUNNER], {
|
|
122
|
+
cwd: root,
|
|
123
|
+
// The project's .env under the CLI's environment, like dokebi start (D-0073).
|
|
124
|
+
env: {
|
|
125
|
+
...appEnvironment(root, 'production'),
|
|
126
|
+
DOKEBI_CLI_MAIL_PREVIEW: mailPreview ? JSON.stringify(mailPreview) : '',
|
|
127
|
+
DOKEBI_CLI_SETTINGS_FILE: settings.path,
|
|
128
|
+
},
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
130
|
+
});
|
|
131
|
+
// The app's own log lines: kept for the error, never printed otherwise (bounded).
|
|
132
|
+
let output = '';
|
|
133
|
+
const keep = (chunk) => {
|
|
134
|
+
output = (output + chunk.toString('utf8')).slice(-16_384);
|
|
135
|
+
};
|
|
136
|
+
child.stdout?.on('data', keep);
|
|
137
|
+
child.stderr?.on('data', keep);
|
|
138
|
+
return await new Promise((resolve, reject) => {
|
|
139
|
+
let message;
|
|
140
|
+
let lingered = false;
|
|
141
|
+
let grace;
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
child.kill('SIGKILL');
|
|
144
|
+
reject(new CliError('DESCRIBE_TIMEOUT', `The app did not describe itself within ${timeoutMs} ms`, {
|
|
145
|
+
details: {
|
|
146
|
+
timeoutMs,
|
|
147
|
+
fix: 'A plugin or the app code waits in setup (a connection, a long task); run with DOKEBI_LOG_LEVEL=debug, or pass --timeout-ms',
|
|
148
|
+
output: tail(output),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
}, timeoutMs);
|
|
152
|
+
child.on('message', (received) => {
|
|
153
|
+
// The runner masks URL credentials itself; core's rule applies on this side as well.
|
|
154
|
+
message = maskTexts(received);
|
|
155
|
+
debug('Describe answer received', { type: message.type });
|
|
156
|
+
grace = setTimeout(() => {
|
|
157
|
+
lingered = true;
|
|
158
|
+
child.kill('SIGKILL');
|
|
159
|
+
}, EXIT_GRACE_MS);
|
|
160
|
+
});
|
|
161
|
+
child.once('error', (error) => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
if (grace)
|
|
164
|
+
clearTimeout(grace);
|
|
165
|
+
reject(new CliError('DESCRIBE_FAILED', 'Unable to run the app to describe it', { cause: error }));
|
|
166
|
+
});
|
|
167
|
+
child.once('exit', (code, signal) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
if (grace)
|
|
170
|
+
clearTimeout(grace);
|
|
171
|
+
debug('Describe process exited', { code, signal, answered: message !== undefined });
|
|
172
|
+
if (message)
|
|
173
|
+
resolve({ message, lingered });
|
|
174
|
+
else
|
|
175
|
+
reject(new CliError('DESCRIBE_FAILED', 'The app exited before it described itself', {
|
|
176
|
+
details: { exit: code ?? signal, output: tail(output) },
|
|
177
|
+
}));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** The last lines of the app's output, URL credentials removed. */
|
|
182
|
+
function tail(output) {
|
|
183
|
+
return output
|
|
184
|
+
.split('\n')
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.slice(-20)
|
|
187
|
+
.map((line) => redaction.maskCredentials(line).slice(0, 2000));
|
|
188
|
+
}
|
|
189
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
190
|
+
? value
|
|
191
|
+
: {};
|
|
192
|
+
/** The CLI result from core's description and the error definitions. */
|
|
193
|
+
export function shapeDescription(name, description, errors, ms, lingered = false) {
|
|
194
|
+
const unavailable = [...description.unavailable];
|
|
195
|
+
const lists = {
|
|
196
|
+
routes: [],
|
|
197
|
+
pages: [],
|
|
198
|
+
jobs: [],
|
|
199
|
+
schedules: [],
|
|
200
|
+
env: [],
|
|
201
|
+
};
|
|
202
|
+
const app = {
|
|
203
|
+
name,
|
|
204
|
+
core: description.core,
|
|
205
|
+
plugins: description.plugins,
|
|
206
|
+
};
|
|
207
|
+
let keys = [];
|
|
208
|
+
let db = {};
|
|
209
|
+
let constraint;
|
|
210
|
+
for (const { section, plugin, value } of description.sections) {
|
|
211
|
+
if (section === 'config' && plugin === 'core') {
|
|
212
|
+
keys = Array.isArray(record(value).keys) ? record(value).keys : [];
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (LISTS.has(section)) {
|
|
216
|
+
const list = lists[section];
|
|
217
|
+
if (Array.isArray(value))
|
|
218
|
+
for (const item of value)
|
|
219
|
+
list.push(section === 'env' ? { ...record(item), plugin } : item);
|
|
220
|
+
else
|
|
221
|
+
unavailable.push({
|
|
222
|
+
plugin,
|
|
223
|
+
section,
|
|
224
|
+
code: 'INVALID_SECTION',
|
|
225
|
+
reason: `The ${section} section must be a list`,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (section === 'db') {
|
|
230
|
+
db = { ...db, ...record(value) };
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (section === 'app') {
|
|
234
|
+
const given = record(value);
|
|
235
|
+
if (typeof given.constraint === 'string')
|
|
236
|
+
constraint = given.constraint;
|
|
237
|
+
for (const [key, item] of Object.entries(given))
|
|
238
|
+
if (key !== 'constraint')
|
|
239
|
+
app[key] = item;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (Object.hasOwn(app, section)) {
|
|
243
|
+
unavailable.push({
|
|
244
|
+
plugin,
|
|
245
|
+
section,
|
|
246
|
+
code: 'DUPLICATE_SECTION',
|
|
247
|
+
reason: `Another plugin already described ${section}`,
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
app[section] = value;
|
|
252
|
+
}
|
|
253
|
+
// The effective login rule of each route (dokebi-auth gives them as auth.routes).
|
|
254
|
+
const rules = new Map();
|
|
255
|
+
const auth = record(app.auth);
|
|
256
|
+
if (Array.isArray(auth.routes)) {
|
|
257
|
+
for (const item of auth.routes) {
|
|
258
|
+
const { method, url, app: owner, ...rule } = record(item);
|
|
259
|
+
rules.set(`${String(method)} ${String(url)} ${typeof owner === 'string' ? owner : ''}`, rule);
|
|
260
|
+
}
|
|
261
|
+
const { routes: _routes, ...rest } = auth;
|
|
262
|
+
app.auth = rest;
|
|
263
|
+
}
|
|
264
|
+
// The app a route belongs to: dokebi-app's route constraint (its name is in the app section).
|
|
265
|
+
const routes = lists.routes.map((route) => {
|
|
266
|
+
const constraints = record(route.constraints);
|
|
267
|
+
const owner = constraint === undefined ? undefined : constraints[constraint];
|
|
268
|
+
const host = constraints.host;
|
|
269
|
+
const rule = rules.get(`${String(route.method)} ${String(route.url)} ${typeof owner === 'string' ? owner : ''}`);
|
|
270
|
+
return {
|
|
271
|
+
...route,
|
|
272
|
+
...(typeof owner === 'string' ? { app: owner } : {}),
|
|
273
|
+
...(typeof host === 'string' ? { host } : {}),
|
|
274
|
+
...(rule === undefined ? {} : { auth: rule }),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
const env = new Map();
|
|
278
|
+
for (const item of lists.env)
|
|
279
|
+
if (typeof item.name === 'string' && !env.has(item.name))
|
|
280
|
+
env.set(item.name, item);
|
|
281
|
+
const definitions = [...(errors.definitions ?? [])];
|
|
282
|
+
if (errors.failure)
|
|
283
|
+
unavailable.push({
|
|
284
|
+
plugin: 'project',
|
|
285
|
+
section: 'errors',
|
|
286
|
+
code: errors.failure.code,
|
|
287
|
+
reason: errors.failure.reason,
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
schemaVersion: DESCRIBE_SCHEMA_VERSION,
|
|
291
|
+
app,
|
|
292
|
+
routes,
|
|
293
|
+
pages: lists.pages,
|
|
294
|
+
jobs: lists.jobs,
|
|
295
|
+
schedules: lists.schedules,
|
|
296
|
+
errors: definitions,
|
|
297
|
+
config: { keys, env: [...env.values()] },
|
|
298
|
+
db: { ...db, unavailable: DB_NOT_READ },
|
|
299
|
+
unavailable,
|
|
300
|
+
...(lingered
|
|
301
|
+
? {
|
|
302
|
+
warnings: [
|
|
303
|
+
{
|
|
304
|
+
code: 'DESCRIBE_PROCESS_LINGERED',
|
|
305
|
+
message: 'The app kept running after it described itself (an open handle) and was stopped',
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
310
|
+
ms,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** `dokebi describe`: loads the project in describe mode and returns the description. */
|
|
314
|
+
export async function describeProject(root, options = {}) {
|
|
315
|
+
const started = Date.now();
|
|
316
|
+
await checkProject(root);
|
|
317
|
+
const core = installedVersion(root, '@dokebi/core');
|
|
318
|
+
if (core && core[0] === 0 && (core[1] < 4 || (core[1] === 4 && core[2] < 2)))
|
|
319
|
+
throw new CliError('DESCRIBE_UNSUPPORTED', `@dokebi/core ${core.join('.')} cannot describe an app`, {
|
|
320
|
+
details: {
|
|
321
|
+
core: core.join('.'),
|
|
322
|
+
fix: 'Install @dokebi/core 0.4.2 or later in the project (npm install @dokebi/core@^0.4.2)',
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const timeoutMs = options.timeoutMs ?? DESCRIBE_TIMEOUT_MS;
|
|
326
|
+
debug('Describing the project', { timeoutMs });
|
|
327
|
+
const { message, lingered } = await run(root, timeoutMs, options.mailPreview);
|
|
328
|
+
if (message.type === 'failure')
|
|
329
|
+
throw new CliError(message.code, `The app could not be described: ${message.message}`, {
|
|
330
|
+
details: {
|
|
331
|
+
...(message.plugin ? { plugin: message.plugin } : {}),
|
|
332
|
+
fix: message.code === 'DESCRIBE_UNSUPPORTED'
|
|
333
|
+
? 'Install @dokebi/core 0.4.2 or later in the project'
|
|
334
|
+
: 'npx dokebi doctor, then fix what the message names (the app fails the same way at start)',
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
const result = shapeDescription(projectName(root), message.description, message.errors, Date.now() - started, lingered);
|
|
338
|
+
if (!options.withDb)
|
|
339
|
+
return result;
|
|
340
|
+
// D-0077 F: a separate step that connects (describe mode itself never does).
|
|
341
|
+
const listed = Array.isArray(result.db.connections)
|
|
342
|
+
? result.db.connections
|
|
343
|
+
: [];
|
|
344
|
+
const connections = await describeDatabases(root, listed, timeoutMs);
|
|
345
|
+
return { ...result, db: { connections }, ms: Date.now() - started };
|
|
346
|
+
}
|
|
347
|
+
/** The db line of the summary. */
|
|
348
|
+
function dbText(db) {
|
|
349
|
+
const connections = Array.isArray(db.connections)
|
|
350
|
+
? db.connections
|
|
351
|
+
: [];
|
|
352
|
+
// D-0127: an optional connection is marked; the default (required) is not.
|
|
353
|
+
const named = (item) => `${String(item.name)}${item.startup === 'optional' ? ' [optional]' : ''}`;
|
|
354
|
+
if (typeof db.unavailable === 'string')
|
|
355
|
+
return `db: ${connections.length ? `${connections.map((item) => `${named(item)} (${String(item.adapter)})`).join(', ')}; ` : ''}${db.unavailable}`;
|
|
356
|
+
if (!connections.length)
|
|
357
|
+
return 'db: no connections';
|
|
358
|
+
return `db: ${connections
|
|
359
|
+
.map((item) => {
|
|
360
|
+
if (typeof item.unavailable === 'string')
|
|
361
|
+
return `${named(item)} unavailable (${item.unavailable})`;
|
|
362
|
+
const tables = Array.isArray(item.tables) ? item.tables : undefined;
|
|
363
|
+
const collections = Array.isArray(item.collections) ? item.collections : [];
|
|
364
|
+
return `${named(item)} (${String(item.dialect)}) ${tables ? `${tables.length} tables` : `${collections.length} collections`}`;
|
|
365
|
+
})
|
|
366
|
+
.join('; ')}`;
|
|
367
|
+
}
|
|
368
|
+
/** Module plugins are `dokebi-module.<name>` (D-0032). */
|
|
369
|
+
const MODULE_PREFIX = 'dokebi-module.';
|
|
370
|
+
/**
|
|
371
|
+
* D-0130: what each module contributes to and who collects it, one line per module point, e.g.
|
|
372
|
+
* `module points: dokebi-module.notes -> dokebi-mcp:tools (collected by dokebi-mcp)`. A point no
|
|
373
|
+
* plugin collects says so: its values are dropped (warn POINT_NOT_COLLECTED at start).
|
|
374
|
+
*/
|
|
375
|
+
export function modulePointsText(plugins) {
|
|
376
|
+
const collectors = new Map();
|
|
377
|
+
for (const plugin of plugins)
|
|
378
|
+
for (const point of plugin.collects ?? [])
|
|
379
|
+
collectors.set(point, [...(collectors.get(point) ?? []), plugin.name]);
|
|
380
|
+
const lines = [];
|
|
381
|
+
for (const plugin of plugins) {
|
|
382
|
+
if (!plugin.name.startsWith(MODULE_PREFIX))
|
|
383
|
+
continue;
|
|
384
|
+
for (const point of plugin.contributes ?? []) {
|
|
385
|
+
const by = collectors.get(point);
|
|
386
|
+
lines.push(`module points: ${plugin.name} -> ${point} (${by ? `collected by ${by.join(', ')}` : 'no plugin collects it: the values are dropped'})`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
391
|
+
/** The short human summary. */
|
|
392
|
+
export function describeText(result) {
|
|
393
|
+
const plugins = result.app.plugins;
|
|
394
|
+
const methods = new Map();
|
|
395
|
+
for (const route of result.routes) {
|
|
396
|
+
const method = String(route.method);
|
|
397
|
+
methods.set(method, (methods.get(method) ?? 0) + 1);
|
|
398
|
+
}
|
|
399
|
+
const guarded = result.routes.filter((route) => record(route.auth).mode === 'required').length;
|
|
400
|
+
const engines = new Map();
|
|
401
|
+
for (const page of result.pages)
|
|
402
|
+
engines.set(String(page.engine), (engines.get(String(page.engine)) ?? 0) + 1);
|
|
403
|
+
const count = (map) => [...map].map(([key, value]) => `${key} ${value}`).join(', ');
|
|
404
|
+
const lines = [
|
|
405
|
+
`app ${result.app.name ?? '(no name)'}: ${plugins.length} plugins, core ${String(result.app.core)}`,
|
|
406
|
+
`routes: ${result.routes.length}${methods.size ? ` (${count(methods)})` : ''}${guarded ? `, ${guarded} need a login` : ''}`,
|
|
407
|
+
`pages: ${result.pages.length}${engines.size ? ` (${count(engines)})` : ''}`,
|
|
408
|
+
`jobs: ${result.jobs.length}, schedules: ${result.schedules.length}, error codes: ${result.errors.length}`,
|
|
409
|
+
`config: ${result.config.keys.length} option keys; environment: ${result.config.env.map((item) => item.name).join(', ') || 'none'}`,
|
|
410
|
+
dbText(result.db),
|
|
411
|
+
...modulePointsText(plugins),
|
|
412
|
+
...result.unavailable.map((item) => `unavailable: ${item.plugin}${item.section ? ` (${item.section})` : ''}: ${item.code} ${item.reason}`),
|
|
413
|
+
...(result.warnings ?? []).map((item) => `warning: ${item.code} ${item.message}`),
|
|
414
|
+
'npx dokebi describe --json has every route, page, job, schedule, error code and option key',
|
|
415
|
+
];
|
|
416
|
+
return `${lines.join('\n')}\n`;
|
|
417
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-0165: the passwords and tokens of the connections in dokebi.yaml, as the variables
|
|
3
|
+
* `dokebi dev-infra up` gives Compose: `DOKEBI_<PLUGIN>_<CONNECTION>_PASSWORD` (and `_TOKEN`), and
|
|
4
|
+
* without the connection for `main` (`DOKEBI_REDIS_PASSWORD`). A password is the one of a URL
|
|
5
|
+
* (`url`, `servers`) or a `password` or `pass` field; placeholders (`<…>`) are none. R5 Codex T4:
|
|
6
|
+
* a Redis with a password in compose.yaml could only get it from `.env` or a script of its own.
|
|
7
|
+
*/
|
|
8
|
+
export declare function connectionSecrets(options: Record<string, unknown>): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Compose gets the passwords of dokebi.yaml in its child environment: the main database's as
|
|
11
|
+
* DOKEBI_DB_PASSWORD, and every connection's (D-0165, `connectionSecrets`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function databaseEnvironment(root: string): Promise<NodeJS.ProcessEnv>;
|
|
14
|
+
/** Fixed actions only: never expose Compose config/env output or delete data volumes. */
|
|
15
|
+
export declare function runDevCompose(root: string, action: string, files?: string[]): Promise<{
|
|
16
|
+
action: string;
|
|
17
|
+
files: string[];
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { loadConfig } from '@dokebi/core';
|
|
4
|
+
import { configTarget, missingConfig } from './configuration.js';
|
|
5
|
+
import { debug, describeError } from './diagnostics.js';
|
|
6
|
+
import { CliError } from './errors.js';
|
|
7
|
+
import { exists, securePath } from './filesystem.js';
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const actions = {
|
|
10
|
+
up: ['up', '-d', '--wait'],
|
|
11
|
+
start: ['start'],
|
|
12
|
+
stop: ['stop'],
|
|
13
|
+
down: ['down'],
|
|
14
|
+
status: ['ps', '--format', 'json'],
|
|
15
|
+
};
|
|
16
|
+
/** A variable name part: `dokebi-redis` → `REDIS`, `cache.eu` → `CACHE_EU`. */
|
|
17
|
+
function variablePart(name) {
|
|
18
|
+
return name
|
|
19
|
+
.replace(/^dokebi-/, '')
|
|
20
|
+
.toUpperCase()
|
|
21
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
22
|
+
.replace(/^_|_$/g, '');
|
|
23
|
+
}
|
|
24
|
+
/** The password of a URL (`redis://:secret@host`), or undefined. */
|
|
25
|
+
function urlPassword(value) {
|
|
26
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const password = new URL(value).password;
|
|
30
|
+
return password ? decodeURIComponent(password) : undefined;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Not a URL after all (the plugin reports its own option): no password to pass.
|
|
34
|
+
debug('A connection value is not a URL', { error: describeError(error) });
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* D-0165: the passwords and tokens of the connections in dokebi.yaml, as the variables
|
|
40
|
+
* `dokebi dev-infra up` gives Compose: `DOKEBI_<PLUGIN>_<CONNECTION>_PASSWORD` (and `_TOKEN`), and
|
|
41
|
+
* without the connection for `main` (`DOKEBI_REDIS_PASSWORD`). A password is the one of a URL
|
|
42
|
+
* (`url`, `servers`) or a `password` or `pass` field; placeholders (`<…>`) are none. R5 Codex T4:
|
|
43
|
+
* a Redis with a password in compose.yaml could only get it from `.env` or a script of its own.
|
|
44
|
+
*/
|
|
45
|
+
export function connectionSecrets(options) {
|
|
46
|
+
const found = {};
|
|
47
|
+
for (const [section, value] of Object.entries(options)) {
|
|
48
|
+
const connections = value?.connections;
|
|
49
|
+
if (!connections || typeof connections !== 'object' || Array.isArray(connections))
|
|
50
|
+
continue;
|
|
51
|
+
for (const [name, connection] of Object.entries(connections)) {
|
|
52
|
+
const secrets = {};
|
|
53
|
+
const walk = (item, key) => {
|
|
54
|
+
if (typeof item === 'string') {
|
|
55
|
+
if (item.trim().startsWith('<'))
|
|
56
|
+
return;
|
|
57
|
+
if (key === 'token')
|
|
58
|
+
secrets.TOKEN ??= item;
|
|
59
|
+
else if (key === 'password' || key === 'pass')
|
|
60
|
+
secrets.PASSWORD ??= item;
|
|
61
|
+
else {
|
|
62
|
+
const password = urlPassword(item);
|
|
63
|
+
if (password !== undefined)
|
|
64
|
+
secrets.PASSWORD ??= password;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (Array.isArray(item))
|
|
68
|
+
for (const entry of item)
|
|
69
|
+
walk(entry, key);
|
|
70
|
+
else if (item && typeof item === 'object')
|
|
71
|
+
for (const [child, entry] of Object.entries(item))
|
|
72
|
+
walk(entry, child);
|
|
73
|
+
};
|
|
74
|
+
walk(connection, '');
|
|
75
|
+
const prefix = `DOKEBI_${variablePart(section)}`;
|
|
76
|
+
for (const [kind, secret] of Object.entries(secrets)) {
|
|
77
|
+
found[`${prefix}_${variablePart(name)}_${kind}`] = secret;
|
|
78
|
+
if (name === 'main')
|
|
79
|
+
found[`${prefix}_${kind}`] = secret;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Compose gets the passwords of dokebi.yaml in its child environment: the main database's as
|
|
87
|
+
* DOKEBI_DB_PASSWORD, and every connection's (D-0165, `connectionSecrets`).
|
|
88
|
+
*/
|
|
89
|
+
export async function databaseEnvironment(root) {
|
|
90
|
+
// M50: the file the app reads; an empty DOKEBI_CONFIG is no value (D-0154 D4), never the
|
|
91
|
+
// project folder itself (`??` kept '' and loaded the folder).
|
|
92
|
+
const target = await configTarget(root);
|
|
93
|
+
// R3 Claude T4 review: a fresh copy without dokebi.yaml got INTERNAL_ERROR ("Unable to read
|
|
94
|
+
// configuration file") from dev-infra up.
|
|
95
|
+
if (!target.fromEnvironment && !(await exists(target.path)))
|
|
96
|
+
throw missingConfig(root, 'No dokebi.yaml: dev-infra up reads the database password from it');
|
|
97
|
+
const config = await loadConfig(target.path);
|
|
98
|
+
const db = config.options['dokebi-db'];
|
|
99
|
+
const value = db?.connections?.main?.options?.url;
|
|
100
|
+
const secrets = connectionSecrets(config.options);
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
return secrets;
|
|
103
|
+
if (typeof value !== 'string' || value.startsWith('<'))
|
|
104
|
+
throw new CliError('DEV_INFRA_CONFIG_INVALID', 'Set the main database URL in dokebi.yaml first', {
|
|
105
|
+
details: {
|
|
106
|
+
// R3 Claude T4 review: the sample's placeholder names `npx dokebi dev-infra`; the error did not.
|
|
107
|
+
fix: 'run npx dokebi dev-infra first: it writes the development database URL into dokebi.yaml (and compose.yaml when there is none); or set your own URL there',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
return { ...secrets, DOKEBI_DB_PASSWORD: decodeURIComponent(new URL(value).password) };
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// dokebi-check: allow missing-cause the URL error carries its input, which holds the database password
|
|
115
|
+
throw new CliError('DEV_INFRA_CONFIG_INVALID', 'The main database URL is invalid');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Fixed actions only: never expose Compose config/env output or delete data volumes. */
|
|
119
|
+
export async function runDevCompose(root, action, files = ['compose.yaml']) {
|
|
120
|
+
if (!Object.hasOwn(actions, action))
|
|
121
|
+
throw new CliError('USAGE', 'dev-infra action must be up, start, stop, down or status');
|
|
122
|
+
if (!files.length)
|
|
123
|
+
throw new CliError('USAGE', 'At least one Compose file is required');
|
|
124
|
+
const paths = await Promise.all(files.map((file) => securePath(root, file)));
|
|
125
|
+
const environment = await databaseEnvironment(root);
|
|
126
|
+
const password = environment.DOKEBI_DB_PASSWORD;
|
|
127
|
+
const mask = (value) => {
|
|
128
|
+
if (!password)
|
|
129
|
+
return value;
|
|
130
|
+
return value
|
|
131
|
+
.replaceAll(password, '<secret>')
|
|
132
|
+
.replaceAll(encodeURIComponent(password), '<secret>');
|
|
133
|
+
};
|
|
134
|
+
debug('Running development infrastructure', { action, files });
|
|
135
|
+
try {
|
|
136
|
+
const { stdout, stderr } = await exec('docker', [
|
|
137
|
+
'compose',
|
|
138
|
+
...paths.flatMap((path) => ['-f', path]),
|
|
139
|
+
...actions[action],
|
|
140
|
+
], {
|
|
141
|
+
cwd: root,
|
|
142
|
+
env: { ...process.env, ...environment },
|
|
143
|
+
timeout: 300_000,
|
|
144
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
145
|
+
});
|
|
146
|
+
return { action, files, stdout: mask(stdout), stderr: mask(stderr) };
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const failure = error;
|
|
150
|
+
throw new CliError('DEV_INFRA_FAILED', mask(failure.stderr || failure.message), {
|
|
151
|
+
details: { action, files },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/dev-guard.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
// Preloaded (`node --import`) into the processes `dokebi dev` starts (D-0044). They get an IPC
|
|
2
2
|
// channel to dev; when dev dies, even by SIGKILL, the channel closes and the process drains
|
|
3
3
|
// (SIGTERM) instead of living on as an orphan that holds the port or keeps building.
|
|
4
|
+
//
|
|
5
|
+
// D-0154: dev drives configuration reloads. It marks the process (core then does not watch
|
|
6
|
+
// dokebi.yaml itself) and, when dokebi.yaml changed, asks over the channel; the app's reload hook
|
|
7
|
+
// (registered by core's Application or @dokebi/cluster's runner once running) reads the file
|
|
8
|
+
// again and the answer says whether dev must restart the app instead.
|
|
9
|
+
const RELOAD_HOOK = Symbol.for('dokebi.config.reload.v1');
|
|
10
|
+
const RELOAD_EXTERNAL = Symbol.for('dokebi.config.reload.external.v1');
|
|
11
|
+
/** The fields of a reload result dev reads: outcome, code and paths, never values. */
|
|
12
|
+
function answerOf(result) {
|
|
13
|
+
const list = (value) => Array.isArray(value) ? value.slice(0, 64).map((item) => String(item)) : undefined;
|
|
14
|
+
return {
|
|
15
|
+
outcome: typeof result.outcome === 'string' ? result.outcome : 'failed',
|
|
16
|
+
code: typeof result.code === 'string' ? result.code : undefined,
|
|
17
|
+
paths: list(result.paths),
|
|
18
|
+
restartRequired: list(result.restartRequired),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
4
21
|
if (process.send) {
|
|
5
22
|
process.once('disconnect', () => process.kill(process.pid, 'SIGTERM'));
|
|
23
|
+
const global = globalThis;
|
|
24
|
+
global[RELOAD_EXTERNAL] = true;
|
|
25
|
+
process.on('message', (message) => {
|
|
26
|
+
const request = message;
|
|
27
|
+
if (request?.dokebi !== 'dev-reload' || typeof request.id !== 'number')
|
|
28
|
+
return;
|
|
29
|
+
const id = request.id;
|
|
30
|
+
const reply = (fields) => {
|
|
31
|
+
try {
|
|
32
|
+
process.send?.({ dokebi: 'dev-reload-result', id, ...fields });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// dokebi-check: allow empty-catch the channel to dev is gone; dev restarts the app when no answer comes
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const hook = global[RELOAD_HOOK];
|
|
39
|
+
if (typeof hook !== 'function') {
|
|
40
|
+
// An app without the hook (an older core, not running yet): dev restarts it.
|
|
41
|
+
reply({ outcome: 'unsupported' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
Promise.resolve()
|
|
45
|
+
.then(() => hook('dev'))
|
|
46
|
+
.then((result) => reply(answerOf(result)),
|
|
47
|
+
// The app logged why (core logs every failed reload); dev restarts it.
|
|
48
|
+
() => reply({ outcome: 'failed', code: 'CONFIG_RELOAD_FAILED' }));
|
|
49
|
+
});
|
|
6
50
|
// The channel must not keep a finished process alive.
|
|
7
51
|
process.channel?.unref();
|
|
8
52
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The answer that asks the CLI for the default development host (`--domain auto`, `--domains auto`). */
|
|
2
|
+
export declare const AUTO_DOMAIN = "auto";
|
|
3
|
+
/** A DNS label (RFC 1035): lower-case letters, digits and inner dashes, at most 63 characters. */
|
|
4
|
+
export declare function hostLabel(value: string): string;
|
|
5
|
+
/** The project's label: from the package.json name, else the folder name, else `dokebi-app`. */
|
|
6
|
+
export declare function projectHostLabel(packageName: unknown, folder: string): string;
|
|
7
|
+
/** `<app>.<project>.localhost`: the development host of one app of one project. */
|
|
8
|
+
export declare function devHost(app: string, project: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The label of the project at `root` (D-0102). Before `init` (a recipe planning a new folder)
|
|
11
|
+
* there is no package.json yet: the folder name is what init writes as the package name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readProjectHostLabel(root: string): Promise<string>;
|