@dokebi/cli 0.4.2 → 0.4.27
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 +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- 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 +638 -37
- 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 +15 -0
- package/dist/check/shared-state-rules.js +117 -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 +381 -36
- 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-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -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/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- 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 +20 -2
- package/dist/install-module.js +45 -8
- 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 +17 -2
- 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 +40 -4
- package/dist/npm-client.js +152 -15
- 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 +195 -16
- 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/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -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 +497 -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 +3006 -333
- 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/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- 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 +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- 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/bin.js
CHANGED
|
@@ -1,18 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// dokebi-check: allow console-output the CLI prints its human and JSON output on stdout and stderr (D-0151)
|
|
3
|
+
// The modules of a command load when that command runs (`await import(…)` in its branch): the
|
|
4
|
+
// project, server, recipe, check and verify modules pull in yaml, semver and @dokebi/core, which
|
|
5
|
+
// doubled the start of `--help`, `--version` and `guide` (about 80 ms against 35 ms) although
|
|
6
|
+
// they need none of it. Only the light modules every command uses are imported here.
|
|
2
7
|
import { writeBanner } from './banner.js';
|
|
3
8
|
import { loadCatalog } from './catalog.js';
|
|
4
|
-
import {
|
|
5
|
-
import { COMMAND_DETAILS, COMMANDS } from './commands.js';
|
|
6
|
-
import { parseSetOption } from './config-edit.js';
|
|
9
|
+
import { COMMAND_DETAILS, COMMANDS, commandHasFlag, DRY_RUN_COMMANDS, dryRunList, } from './commands.js';
|
|
7
10
|
import { CliError } from './errors.js';
|
|
8
11
|
import { buildGuide, HELP_POINTER } from './guide.js';
|
|
9
12
|
import { CLI_VERSION, envelope, failure } from './output.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/** A Project, with its module (and what it imports) loaded on first use. */
|
|
14
|
+
async function newProject() {
|
|
15
|
+
const { Project } = await import('./project.js');
|
|
16
|
+
return new Project();
|
|
17
|
+
}
|
|
18
|
+
/** `--set` and config paths are parsed by config-edit.js, which loads yaml. */
|
|
19
|
+
/** Bytes of a secret `config secret --stdin` reads at most. */
|
|
20
|
+
const MAX_SECRET_INPUT = 65_536;
|
|
21
|
+
/**
|
|
22
|
+
* The secret of `config secret --stdin` (D-0151 follow-up): piped stdin up to its end, or on a
|
|
23
|
+
* terminal one line typed without echo. One line; the final line break is not part of it. The
|
|
24
|
+
* value is never printed or logged.
|
|
25
|
+
*/
|
|
26
|
+
async function readSecretInput() {
|
|
27
|
+
const stdin = process.stdin;
|
|
28
|
+
const tooLarge = () => new CliError('INVALID_SET', 'The secret on stdin is larger than 64 KiB', {
|
|
29
|
+
phase: 'arguments',
|
|
30
|
+
});
|
|
31
|
+
let text = '';
|
|
32
|
+
if (stdin.isTTY) {
|
|
33
|
+
process.stderr.write('Secret (input hidden, Enter to finish): ');
|
|
34
|
+
text = await new Promise((resolve, reject) => {
|
|
35
|
+
let typed = '';
|
|
36
|
+
const finish = (error) => {
|
|
37
|
+
stdin.setRawMode(false);
|
|
38
|
+
stdin.off('data', onData);
|
|
39
|
+
stdin.pause();
|
|
40
|
+
process.stderr.write('\n');
|
|
41
|
+
if (error)
|
|
42
|
+
reject(error);
|
|
43
|
+
else
|
|
44
|
+
resolve(typed);
|
|
45
|
+
};
|
|
46
|
+
const onData = (chunk) => {
|
|
47
|
+
for (const char of chunk) {
|
|
48
|
+
if (char === '\r' || char === '\n' || char === '\u0004') {
|
|
49
|
+
finish();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (char === '\u0003') {
|
|
53
|
+
finish(new CliError('USAGE', 'config secret --stdin was interrupted'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
typed = char === '\u007f' || char === '\b' ? typed.slice(0, -1) : typed + char;
|
|
57
|
+
if (Buffer.byteLength(typed) > MAX_SECRET_INPUT) {
|
|
58
|
+
finish(tooLarge());
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
stdin.setRawMode(true);
|
|
64
|
+
stdin.setEncoding('utf8');
|
|
65
|
+
stdin.on('data', onData);
|
|
66
|
+
stdin.resume();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
for await (const chunk of stdin) {
|
|
73
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
74
|
+
size += buffer.length;
|
|
75
|
+
if (size > MAX_SECRET_INPUT)
|
|
76
|
+
throw tooLarge();
|
|
77
|
+
chunks.push(buffer);
|
|
78
|
+
}
|
|
79
|
+
text = Buffer.concat(chunks).toString('utf8');
|
|
80
|
+
}
|
|
81
|
+
const value = text.replace(/\r?\n$/, '');
|
|
82
|
+
if (!value || /[\r\n]/.test(value))
|
|
83
|
+
throw new CliError('INVALID_SET', 'config secret --stdin takes one line on stdin (the secret); it got none or several', {
|
|
84
|
+
phase: 'arguments',
|
|
85
|
+
details: { fix: 'pipe the value from a file or a password manager, or type it when asked' },
|
|
86
|
+
});
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
async function setParser() {
|
|
90
|
+
return (await import('./config-edit.js')).parseSetOption;
|
|
91
|
+
}
|
|
16
92
|
/** The usage lines only (their format is stable; tools parse them). */
|
|
17
93
|
const USAGE_LINES = COMMANDS.map((item) => item.usage).join('\n') + '\n';
|
|
18
94
|
const HELP = `${HELP_POINTER}\n${COMMANDS.map((item) => `${item.usage}\n ${item.summary}`).join('\n')}\n\ndokebi <command> --help shows one command; dokebi --version prints the CLI version.\n`;
|
|
@@ -102,7 +178,7 @@ function flags(input, allowed) {
|
|
|
102
178
|
const value = input[++index];
|
|
103
179
|
if (value === undefined || value.startsWith('--'))
|
|
104
180
|
throw new CliError('USAGE', USAGE_LINES);
|
|
105
|
-
sets.push(
|
|
181
|
+
sets.push(allowed.set(value));
|
|
106
182
|
}
|
|
107
183
|
else if (arg === '--to' && allowed.to && to === undefined) {
|
|
108
184
|
const value = input[++index];
|
|
@@ -116,6 +192,12 @@ function flags(input, allowed) {
|
|
|
116
192
|
throw new CliError('USAGE', USAGE_LINES);
|
|
117
193
|
add.adapter = value;
|
|
118
194
|
}
|
|
195
|
+
else if (arg === '--startup' && allowed.add && add.startup === undefined) {
|
|
196
|
+
const value = input[++index];
|
|
197
|
+
if (value !== 'required' && value !== 'optional')
|
|
198
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
199
|
+
add.startup = value;
|
|
200
|
+
}
|
|
119
201
|
else if (arg === '--no-first-app' && allowed.add && add.firstApp === undefined)
|
|
120
202
|
add.firstApp = false;
|
|
121
203
|
else
|
|
@@ -156,7 +238,30 @@ async function main() {
|
|
|
156
238
|
: value + (value.endsWith('\n') ? '' : '\n'));
|
|
157
239
|
return;
|
|
158
240
|
}
|
|
241
|
+
// AI-M04 F8: --dry-run on a command without it names the commands that take it.
|
|
242
|
+
if (args.includes('--dry-run')) {
|
|
243
|
+
const known = (name) => COMMANDS.some((item) => item.name === name);
|
|
244
|
+
const sub = args[0] && !args[0].startsWith('-') ? `${command} ${args[0]}` : undefined;
|
|
245
|
+
const name = sub && known(sub) ? sub : known(command) ? command : undefined;
|
|
246
|
+
if (name && !commandHasFlag(name, '--dry-run'))
|
|
247
|
+
throw new CliError('USAGE', `dokebi ${name} takes no --dry-run; only these commands do: ${dryRunList()}\n${USAGE_LINES}`, {
|
|
248
|
+
phase: 'arguments',
|
|
249
|
+
details: {
|
|
250
|
+
command: name,
|
|
251
|
+
dryRunCommands: DRY_RUN_COMMANDS,
|
|
252
|
+
hint: `dokebi ${name} --help shows its flags`,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
159
256
|
if (command === 'guide') {
|
|
257
|
+
if (args.includes('--sync')) {
|
|
258
|
+
if (args.some((arg) => arg !== '--sync' && arg !== '--dry-run'))
|
|
259
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
260
|
+
const dryRun = args.includes('--dry-run');
|
|
261
|
+
mutationStarted = !dryRun;
|
|
262
|
+
print(command, await (await newProject()).syncGuide(process.cwd(), dryRun));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
160
265
|
if (args.length)
|
|
161
266
|
throw new CliError('USAGE', USAGE_LINES);
|
|
162
267
|
const guide = buildGuide(await loadCatalog());
|
|
@@ -172,22 +277,142 @@ async function main() {
|
|
|
172
277
|
await recipeCommand(args);
|
|
173
278
|
return;
|
|
174
279
|
}
|
|
280
|
+
if (command === 'mail') {
|
|
281
|
+
const subcommand = args.shift();
|
|
282
|
+
if (subcommand !== 'preview')
|
|
283
|
+
throw unknownCommand(subcommand ?? '', ['preview'], 'mail ');
|
|
284
|
+
const name = args.shift();
|
|
285
|
+
if (!name || name.startsWith('-'))
|
|
286
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
287
|
+
let locale;
|
|
288
|
+
if (args[0] === '--locale' && args[1] && !args[1].startsWith('-')) {
|
|
289
|
+
args.shift();
|
|
290
|
+
locale = args.shift();
|
|
291
|
+
}
|
|
292
|
+
if (args.length)
|
|
293
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
294
|
+
const { describeProject } = await import('./describe.js');
|
|
295
|
+
const described = await describeProject(process.cwd(), {
|
|
296
|
+
mailPreview: { name, ...(locale ? { locale } : {}) },
|
|
297
|
+
});
|
|
298
|
+
const preview = described.app['mail-preview'];
|
|
299
|
+
if (!preview) {
|
|
300
|
+
const unavailable = described.unavailable.find((item) => item.plugin === 'cli-mail-preview' || item.plugin === 'dokebi-mail');
|
|
301
|
+
throw new CliError(unavailable?.code ?? 'MAIL_PREVIEW_FAILED', unavailable?.reason ?? 'Mail preview is unavailable');
|
|
302
|
+
}
|
|
303
|
+
print('mail preview', preview, JSON.stringify(preview, null, 2));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (command === 'verify' && args.includes('--remote')) {
|
|
307
|
+
// D-0184: a deployed site only; nothing local runs.
|
|
308
|
+
const at = args.indexOf('--remote');
|
|
309
|
+
const url = args[at + 1];
|
|
310
|
+
if (url === undefined || url.startsWith('-'))
|
|
311
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
312
|
+
const options = serverFlags(args.filter((_, index) => index !== at && index !== at + 1), { '--timeout-ms': [100, 120_000] }, []);
|
|
313
|
+
const { remoteVerify, remoteVerifyText } = await import('./remote-verify.js');
|
|
314
|
+
const timeoutMs = options.numbers.get('--timeout-ms');
|
|
315
|
+
const result = await remoteVerify(url, {
|
|
316
|
+
root: process.cwd(),
|
|
317
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
318
|
+
});
|
|
319
|
+
print('verify', result, remoteVerifyText(result), result.ok);
|
|
320
|
+
if (!result.ok)
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (command === 'deploy') {
|
|
325
|
+
await deployCommand(args);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
175
328
|
if (command === 'verify') {
|
|
176
|
-
const options = serverFlags(args, { '--wait-ms': undefined }, []);
|
|
329
|
+
const options = serverFlags(args, { '--wait-ms': undefined, '--test-timeout-ms': [1000, 3_600_000] }, []);
|
|
177
330
|
const waitMs = options.numbers.get('--wait-ms');
|
|
178
|
-
const
|
|
331
|
+
const testTimeoutMs = options.numbers.get('--test-timeout-ms');
|
|
332
|
+
const [{ verify }, { testLines }, { TELL_USER_RELAY }] = await Promise.all([
|
|
333
|
+
import('./verify.js'),
|
|
334
|
+
import('./verify-tests.js'),
|
|
335
|
+
import('./recipe-record.js'),
|
|
336
|
+
]);
|
|
337
|
+
const result = await verify(process.cwd(), {
|
|
338
|
+
...(waitMs !== undefined ? { waitMs } : {}),
|
|
339
|
+
...(testTimeoutMs !== undefined ? { testTimeoutMs } : {}),
|
|
340
|
+
});
|
|
179
341
|
print(command, result, `${result.checks
|
|
180
|
-
.map((check) => `${!check.ok ? 'FAIL' : check.skipped ? '-' : 'ok'} ${check.name} (${check.ms} ms)${check.skipped ? `: skipped, ${check.skipped}` : ''}${check.ok ? '' : `: ${check.code ?? ''} ${check.message ?? ''}`}`)
|
|
181
|
-
.join('\n')}\n${result.ok ? 'verify ok' : 'verify FAILED'}${addressLines(result)}${result.
|
|
182
|
-
|
|
342
|
+
.map((check) => `${!check.ok ? 'FAIL' : check.skipped ? '-' : 'ok'} ${check.name} (${check.ms} ms)${check.skipped ? `: skipped, ${check.skipped}` : ''}${check.ok ? '' : `: ${check.code ?? ''} ${check.message ?? ''}`}${check.name === 'test' && result.tests ? testLines(result.tests) : ''}`)
|
|
343
|
+
.join('\n')}\n${result.ok ? 'verify ok' : 'verify FAILED'}${addressLines(result)}${(result.warnings ?? [])
|
|
344
|
+
.map((warning) => `\nwarning: ${warning.code} (${warning.path}): ${warning.hint}`)
|
|
345
|
+
.join('')}${
|
|
346
|
+
// A taken port is the user's call (AI-M04 F5), as in the recipe preflight.
|
|
347
|
+
result.question
|
|
348
|
+
? `\n${result.waitForUser ? 'ask the user' : 'question'}: ${result.question}\nnext: ${result.next}`
|
|
349
|
+
: ''}${result.tellUser
|
|
350
|
+
? `\nTell the user exactly these lines. ${TELL_USER_RELAY.instruction}\n${result.tellUser.map((line) => `- ${line}`).join('\n')}`
|
|
351
|
+
: ''}${
|
|
352
|
+
// D-0105 §4: what the agent writes into its final message itself (not copied).
|
|
353
|
+
result.finalReport
|
|
354
|
+
? `\nYour final message must also contain:\n${result.finalReport.map((line) => `- ${line}`).join('\n')}`
|
|
183
355
|
: ''}`, result.ok);
|
|
184
356
|
if (!result.ok)
|
|
185
357
|
process.exitCode = 1;
|
|
186
358
|
return;
|
|
187
359
|
}
|
|
360
|
+
if (command === 'describe') {
|
|
361
|
+
// Read only (D-0077 B): the app's setup in describe mode; nothing listens or connects.
|
|
362
|
+
// D-0077 C: --openapi [--app <name>] prints OpenAPI 3.1 documents instead.
|
|
363
|
+
const appAt = args.indexOf('--app');
|
|
364
|
+
const appName = appAt < 0 ? undefined : args[appAt + 1];
|
|
365
|
+
if (appAt >= 0 && (appName === undefined || appName.startsWith('-')))
|
|
366
|
+
throw new CliError('USAGE', `--app needs an app name\n${USAGE_LINES}`);
|
|
367
|
+
const rest = appAt < 0 ? args : args.filter((_, index) => index !== appAt && index !== appAt + 1);
|
|
368
|
+
const options = serverFlags(rest, { '--timeout-ms': [1000, 600_000] }, [
|
|
369
|
+
'--with-db',
|
|
370
|
+
'--openapi',
|
|
371
|
+
]);
|
|
372
|
+
if (appName !== undefined && !options.switches.has('--openapi'))
|
|
373
|
+
throw new CliError('USAGE', `--app is for --openapi\n${USAGE_LINES}`);
|
|
374
|
+
const timeoutMs = options.numbers.get('--timeout-ms');
|
|
375
|
+
const { describeProject, describeText } = await import('./describe.js');
|
|
376
|
+
const result = await describeProject(process.cwd(), {
|
|
377
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
378
|
+
// D-0077 F: --with-db also reads the tables (a separate step that connects).
|
|
379
|
+
withDb: options.switches.has('--with-db'),
|
|
380
|
+
});
|
|
381
|
+
if (options.switches.has('--openapi')) {
|
|
382
|
+
const { openApi, projectVersion } = await import('./openapi.js');
|
|
383
|
+
const version = projectVersion(process.cwd());
|
|
384
|
+
const documents = openApi(result, {
|
|
385
|
+
...(appName === undefined ? {} : { app: appName }),
|
|
386
|
+
...(version === undefined ? {} : { version }),
|
|
387
|
+
});
|
|
388
|
+
print(command, documents, JSON.stringify(documents, null, 2));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
print(command, result, describeText(result));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (command === 'drift') {
|
|
395
|
+
// D-0120: describe --with-db (it connects) against an expected file of the describe shape.
|
|
396
|
+
const at = args.indexOf('--expected');
|
|
397
|
+
const expected = at < 0 ? undefined : args[at + 1];
|
|
398
|
+
if (expected === undefined || expected.startsWith('-'))
|
|
399
|
+
throw new CliError('USAGE', `--expected needs a file\n${USAGE_LINES}`);
|
|
400
|
+
const options = serverFlags(args.filter((_, index) => index !== at && index !== at + 1), { '--timeout-ms': [1000, 600_000] }, []);
|
|
401
|
+
const timeoutMs = options.numbers.get('--timeout-ms');
|
|
402
|
+
const { drift, driftText } = await import('./drift.js');
|
|
403
|
+
const result = await drift(process.cwd(), {
|
|
404
|
+
expected,
|
|
405
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
406
|
+
});
|
|
407
|
+
print(command, result, driftText(result), result.ok);
|
|
408
|
+
if (!result.ok)
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
188
412
|
if (command === 'check') {
|
|
189
413
|
if (args.length)
|
|
190
414
|
throw new CliError('USAGE', USAGE_LINES);
|
|
415
|
+
const { check, checkText } = await import('./check/index.js');
|
|
191
416
|
const result = await check(process.cwd());
|
|
192
417
|
print(command, result, checkText(result), result.ok);
|
|
193
418
|
if (!result.ok)
|
|
@@ -196,23 +421,140 @@ async function main() {
|
|
|
196
421
|
}
|
|
197
422
|
if (command === 'config') {
|
|
198
423
|
const action = args.shift();
|
|
424
|
+
const parseSetOption = await setParser();
|
|
425
|
+
if (action === 'show') {
|
|
426
|
+
// Read only: secrets are hidden in every mode (config-show.ts).
|
|
427
|
+
if (args.length > 1 || args[0]?.startsWith('-'))
|
|
428
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
429
|
+
const target = args[0];
|
|
430
|
+
const dot = target?.indexOf('.') ?? -1;
|
|
431
|
+
if (target !== undefined && (dot === 0 || target.includes('=')))
|
|
432
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
433
|
+
const section = target === undefined ? undefined : dot > 0 ? target.slice(0, dot) : target;
|
|
434
|
+
// The path is checked like a --set path (segments, no prototype keys).
|
|
435
|
+
const path = target && dot > 0 ? parseSetOption(`${target.slice(dot + 1)}=0`).path : [];
|
|
436
|
+
const { configShow, configShowText } = await import('./config-show.js');
|
|
437
|
+
const result = await configShow(process.cwd(), section === undefined ? undefined : { section, path });
|
|
438
|
+
print('config show', result, configShowText(result));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (action === 'diff') {
|
|
442
|
+
// Read only (AI-M45): option paths of dokebi.yaml against dokebi.example.yaml, no values.
|
|
443
|
+
// --write (AI-M46) copies the paths only in dokebi.yaml into the sample, without secrets.
|
|
444
|
+
let write = false;
|
|
445
|
+
let dryRun = false;
|
|
446
|
+
let prune = false;
|
|
447
|
+
let values = false;
|
|
448
|
+
const rest = [];
|
|
449
|
+
for (const arg of args) {
|
|
450
|
+
if (arg === '--write' && !write)
|
|
451
|
+
write = true;
|
|
452
|
+
else if (arg === '--dry-run' && !dryRun)
|
|
453
|
+
dryRun = true;
|
|
454
|
+
else if (arg === '--prune' && !prune)
|
|
455
|
+
prune = true;
|
|
456
|
+
else if (arg === '--values' && !values)
|
|
457
|
+
values = true;
|
|
458
|
+
else
|
|
459
|
+
rest.push(arg);
|
|
460
|
+
}
|
|
461
|
+
if ((dryRun || prune || values) && !write)
|
|
462
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
463
|
+
if (rest.length > 1 || rest[0]?.startsWith('-') || rest[0]?.includes('.'))
|
|
464
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
465
|
+
const { configDiff, configDiffText } = await import('./config-diff.js');
|
|
466
|
+
if (write) {
|
|
467
|
+
mutationStarted = true;
|
|
468
|
+
const result = await (await newProject()).configSample(process.cwd(), rest[0], dryRun, prune, values);
|
|
469
|
+
const listed = (label, paths) => paths.length
|
|
470
|
+
? `${label} [${paths.length}]:\n${paths.map((path) => ` ${path}`).join('\n')}\n`
|
|
471
|
+
: '';
|
|
472
|
+
print('config diff', result, `${listed('copied into dokebi.example.yaml', result.written) || 'nothing to copy into dokebi.example.yaml\n'}${values ? listed('values written into dokebi.example.yaml', result.valuesWritten ?? []) || 'no value to write into dokebi.example.yaml\n' : ''}${listed('removed from dokebi.example.yaml', result.pruned)}${configDiffText(result.diff)}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const result = await configDiff(process.cwd(), rest[0]);
|
|
476
|
+
print('config diff', result, configDiffText(result));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (action === 'init') {
|
|
480
|
+
// D-0163: a fresh copy of the project (a clone) has no dokebi.yaml; written from the sample.
|
|
481
|
+
const dryRun = args.includes('--dry-run');
|
|
482
|
+
if (args.some((arg) => arg !== '--dry-run'))
|
|
483
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
484
|
+
mutationStarted = true;
|
|
485
|
+
const result = await (await newProject()).configInit(process.cwd(), dryRun);
|
|
486
|
+
print('config init', result);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (action === 'secrets') {
|
|
490
|
+
// D-0184: read only; names, never values.
|
|
491
|
+
if (args.length)
|
|
492
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
493
|
+
const { configSecrets, configSecretsText } = await import('./config-secrets.js');
|
|
494
|
+
const result = await configSecrets(process.cwd());
|
|
495
|
+
const ok = result.missing.length === 0;
|
|
496
|
+
print('config secrets', result, configSecretsText(result), ok);
|
|
497
|
+
if (!ok)
|
|
498
|
+
process.exitCode = 1;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
199
501
|
if (action === 'secret') {
|
|
200
502
|
const dryRun = args.includes('--dry-run');
|
|
201
|
-
const
|
|
503
|
+
const rotate = args.includes('--rotate');
|
|
504
|
+
const fromStdin = args.includes('--stdin');
|
|
505
|
+
const rest = args.filter((arg) => arg !== '--dry-run' && arg !== '--rotate' && arg !== '--stdin');
|
|
202
506
|
const target = rest[0];
|
|
203
507
|
const dot = target?.indexOf('.') ?? -1;
|
|
204
508
|
if (rest.length !== 1 || !target || dot <= 0 || target.includes('='))
|
|
205
509
|
throw new CliError('USAGE', USAGE_LINES);
|
|
206
510
|
// The path is checked like a --set path (segments, no prototype keys).
|
|
207
511
|
const path = parseSetOption(`${target.slice(dot + 1)}=0`).path;
|
|
512
|
+
// --stdin: a secret someone else issued (a log receiver's token), never on the command line.
|
|
513
|
+
const value = fromStdin ? await readSecretInput() : undefined;
|
|
208
514
|
mutationStarted = true;
|
|
209
|
-
const result = await
|
|
515
|
+
const result = await (await newProject()).configSecret(process.cwd(), target.slice(0, dot), path, dryRun, rotate, value);
|
|
210
516
|
print('config secret', result);
|
|
211
517
|
return;
|
|
212
518
|
}
|
|
519
|
+
if (action === 'unset') {
|
|
520
|
+
// D-0153: <plugin-id>.<path>...; never a value (no `=`). A section alone (no path) only
|
|
521
|
+
// for one no installed plugin reads (an old section left in the files).
|
|
522
|
+
let dryRun = false;
|
|
523
|
+
const targets = [];
|
|
524
|
+
for (const arg of args) {
|
|
525
|
+
if (arg === '--dry-run' && !dryRun)
|
|
526
|
+
dryRun = true;
|
|
527
|
+
else if (!arg.startsWith('-')) {
|
|
528
|
+
if (arg.includes('='))
|
|
529
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
530
|
+
const dot = arg.indexOf('.');
|
|
531
|
+
if (dot === 0)
|
|
532
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
533
|
+
// The path is checked like a --set path (segments, no prototype keys).
|
|
534
|
+
if (dot < 0) {
|
|
535
|
+
parseSetOption(`${arg}=0`);
|
|
536
|
+
targets.push({ plugin: arg, path: [] });
|
|
537
|
+
}
|
|
538
|
+
else
|
|
539
|
+
targets.push({
|
|
540
|
+
plugin: arg.slice(0, dot),
|
|
541
|
+
path: parseSetOption(`${arg.slice(dot + 1)}=0`).path,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else
|
|
545
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
546
|
+
}
|
|
547
|
+
if (!targets.length)
|
|
548
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
549
|
+
mutationStarted = true;
|
|
550
|
+
const result = await (await newProject()).configUnset(process.cwd(), targets, dryRun);
|
|
551
|
+
printWarnings(result.warnings);
|
|
552
|
+
print('config unset', result);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
213
555
|
if (action !== 'set') {
|
|
214
556
|
if (action && !action.startsWith('-'))
|
|
215
|
-
throw unknownCommand(action, ['
|
|
557
|
+
throw unknownCommand(action, ['diff', 'init', 'secret', 'secrets', 'set', 'show', 'unset'], 'config ');
|
|
216
558
|
throw new CliError('USAGE', USAGE_LINES);
|
|
217
559
|
}
|
|
218
560
|
let dryRun = false;
|
|
@@ -234,17 +576,32 @@ async function main() {
|
|
|
234
576
|
if (!assignments.length)
|
|
235
577
|
throw new CliError('USAGE', USAGE_LINES);
|
|
236
578
|
mutationStarted = true;
|
|
237
|
-
const result = await
|
|
579
|
+
const result = await (await newProject()).configSet(process.cwd(), assignments, dryRun);
|
|
238
580
|
printWarnings(result.warnings);
|
|
239
581
|
print('config set', result);
|
|
240
582
|
return;
|
|
241
583
|
}
|
|
242
584
|
if (command === 'dev-infra') {
|
|
585
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
586
|
+
const action = args[0];
|
|
587
|
+
args.shift();
|
|
588
|
+
const files = [];
|
|
589
|
+
while (args.length) {
|
|
590
|
+
if (args.shift() !== '--file' || !args[0])
|
|
591
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
592
|
+
files.push(args[0]);
|
|
593
|
+
args.shift();
|
|
594
|
+
}
|
|
595
|
+
mutationStarted = action !== 'status';
|
|
596
|
+
const { runDevCompose } = await import('./dev-compose.js');
|
|
597
|
+
print(command, await runDevCompose(process.cwd(), action, files.length ? files : undefined));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
243
600
|
const dryRun = args.includes('--dry-run');
|
|
244
601
|
const options = serverFlags(args.filter((arg) => arg !== '--dry-run'), { '--db-port': [1, 65535] }, []);
|
|
245
602
|
mutationStarted = true;
|
|
246
603
|
const dbPort = options.numbers.get('--db-port');
|
|
247
|
-
const result = await
|
|
604
|
+
const result = await (await newProject()).devInfra(process.cwd(), {
|
|
248
605
|
dryRun,
|
|
249
606
|
...(dbPort !== undefined ? { dbPort } : {}),
|
|
250
607
|
});
|
|
@@ -252,12 +609,31 @@ async function main() {
|
|
|
252
609
|
return;
|
|
253
610
|
}
|
|
254
611
|
if (command === 'inspect' || command === 'doctor') {
|
|
255
|
-
const verbose = args.
|
|
256
|
-
|
|
612
|
+
const verbose = args.includes('--verbose');
|
|
613
|
+
// D-0184: doctor --production adds the checks of a server.
|
|
614
|
+
const production = command === 'doctor' && args.includes('--production');
|
|
615
|
+
if (args.some((arg) => arg !== '--verbose' && !(arg === '--production' && production)) ||
|
|
616
|
+
new Set(args).size !== args.length)
|
|
257
617
|
throw new CliError('USAGE', USAGE_LINES);
|
|
618
|
+
const { ProjectInspector } = await import('./project-inspector.js');
|
|
258
619
|
const result = await new ProjectInspector().inspect(process.cwd(), {
|
|
259
620
|
runChecks: command === 'doctor',
|
|
260
621
|
});
|
|
622
|
+
if (production) {
|
|
623
|
+
const { productionDiagnostics } = await import('./production-doctor.js');
|
|
624
|
+
result.diagnostics.push(...(await productionDiagnostics(process.cwd())));
|
|
625
|
+
result.healthy = !result.diagnostics.some((item) => item.severity === 'error');
|
|
626
|
+
const codes = {};
|
|
627
|
+
for (const item of result.diagnostics)
|
|
628
|
+
codes[item.code] = (codes[item.code] ?? 0) + 1;
|
|
629
|
+
result.summary = {
|
|
630
|
+
...result.summary,
|
|
631
|
+
healthy: result.healthy,
|
|
632
|
+
errors: result.diagnostics.filter((item) => item.severity === 'error').length,
|
|
633
|
+
warnings: result.diagnostics.filter((item) => item.severity === 'warning').length,
|
|
634
|
+
codes,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
261
637
|
const ok = command === 'inspect' || result.healthy;
|
|
262
638
|
// --json is always complete (summary first); people get the short form unless --verbose.
|
|
263
639
|
process.stdout.write(machine
|
|
@@ -273,7 +649,7 @@ async function main() {
|
|
|
273
649
|
if (args.length && (args.length !== 1 || args[0] !== '--dry-run'))
|
|
274
650
|
throw new CliError('USAGE', USAGE_LINES);
|
|
275
651
|
mutationStarted = true;
|
|
276
|
-
const result = await
|
|
652
|
+
const result = await (await newProject()).migrate(process.cwd(), args[0] === '--dry-run');
|
|
277
653
|
process.stdout.write(JSON.stringify(machine ? envelope(command, result) : result, null, 2) + '\n');
|
|
278
654
|
return;
|
|
279
655
|
}
|
|
@@ -281,7 +657,7 @@ async function main() {
|
|
|
281
657
|
await app(args);
|
|
282
658
|
return;
|
|
283
659
|
}
|
|
284
|
-
if (['dev', 'start', 'stop', 'status', 'logs'].includes(command)) {
|
|
660
|
+
if (['dev', 'start', 'stop', 'reload', 'status', 'logs'].includes(command)) {
|
|
285
661
|
await serverCommand(args);
|
|
286
662
|
return;
|
|
287
663
|
}
|
|
@@ -289,14 +665,19 @@ async function main() {
|
|
|
289
665
|
await moduleCommand(args);
|
|
290
666
|
return;
|
|
291
667
|
}
|
|
668
|
+
if (command === 'settings') {
|
|
669
|
+
await settingsCommand(args);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
292
672
|
if (!['init', 'add', 'upgrade', 'remove'].includes(command))
|
|
293
673
|
throw unknownCommand(command, [...TOP_LEVEL, '--help', '--version']);
|
|
294
674
|
const target = args.shift();
|
|
295
675
|
if (!target || target.startsWith('-'))
|
|
296
676
|
throw new CliError('USAGE', USAGE_LINES);
|
|
297
|
-
const project =
|
|
677
|
+
const project = await newProject();
|
|
298
678
|
let result;
|
|
299
679
|
if (command === 'init') {
|
|
680
|
+
let deploy = false;
|
|
300
681
|
let core;
|
|
301
682
|
let dryRun = false;
|
|
302
683
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -306,6 +687,8 @@ async function main() {
|
|
|
306
687
|
if (!core || core.startsWith('-'))
|
|
307
688
|
throw new CliError('USAGE', USAGE_LINES);
|
|
308
689
|
}
|
|
690
|
+
else if (arg === '--deploy' && !deploy)
|
|
691
|
+
deploy = true;
|
|
309
692
|
else if (arg === '--dry-run' && !dryRun)
|
|
310
693
|
dryRun = true;
|
|
311
694
|
else
|
|
@@ -313,12 +696,18 @@ async function main() {
|
|
|
313
696
|
}
|
|
314
697
|
writeBanner(process.stderr, machine);
|
|
315
698
|
mutationStarted = !dryRun;
|
|
316
|
-
result = await project.init(target, core, dryRun);
|
|
699
|
+
result = await project.init(target, core, dryRun, deploy);
|
|
317
700
|
}
|
|
318
701
|
else if (command === 'add') {
|
|
319
|
-
const options = flags(args, { set:
|
|
702
|
+
const options = flags(args, { set: await setParser(), add: true });
|
|
320
703
|
mutationStarted = true;
|
|
321
704
|
result = await project.add(process.cwd(), target, options.dryRun, options.sets, options.add);
|
|
705
|
+
// D-0098: the first app of the apps host is EJS; its view is recorded (chooser unstated).
|
|
706
|
+
const firstApp = result.firstApp;
|
|
707
|
+
if (firstApp) {
|
|
708
|
+
const { recordAppView } = await import('./view-choice.js');
|
|
709
|
+
result = await recordAppView(process.cwd(), result, firstApp, 'ejs', {}, options.dryRun);
|
|
710
|
+
}
|
|
322
711
|
}
|
|
323
712
|
else if (command === 'upgrade') {
|
|
324
713
|
const options = flags(args, { to: true });
|
|
@@ -354,15 +743,16 @@ function printWarnings(warnings) {
|
|
|
354
743
|
if (machine)
|
|
355
744
|
return;
|
|
356
745
|
for (const warning of warnings ?? [])
|
|
357
|
-
if (warning.code === '
|
|
358
|
-
process.stderr.write(`warning: ${warning.path} looks secret; command-line values can remain in shell history\n`);
|
|
359
|
-
else if (warning.code === 'THIRD_PARTY_PACKAGE')
|
|
746
|
+
if (warning.code === 'THIRD_PARTY_PACKAGE')
|
|
360
747
|
process.stderr.write(`warning: ${warning.path} is not a @dokebi package; install it only with the user's consent\n`);
|
|
748
|
+
else if (warning.code === 'DEPRECATED_PLAN_FIELD')
|
|
749
|
+
process.stderr.write(`warning: ${warning.plugin} uses the deprecated install plan field ${warning.deprecated} (removed in 0.5); upgrade the plugin\n`);
|
|
361
750
|
}
|
|
362
751
|
/** `dokebi plugins list|search|info|doc` (D-0065). */
|
|
363
752
|
async function pluginsCommand(input) {
|
|
364
753
|
const action = input.shift();
|
|
365
754
|
const name = `plugins ${action}`;
|
|
755
|
+
const { pluginsDoc, pluginsInfo, pluginsList, pluginsSearch } = await import('./plugins-command.js');
|
|
366
756
|
if (action === 'list') {
|
|
367
757
|
let compatible = false;
|
|
368
758
|
let online = false;
|
|
@@ -413,6 +803,7 @@ async function recipeCommand(input) {
|
|
|
413
803
|
const name = input.shift();
|
|
414
804
|
if (!name || name.startsWith('-'))
|
|
415
805
|
throw new CliError('USAGE', USAGE_LINES);
|
|
806
|
+
const { listRecipes, runRecipe } = await import('./recipe.js');
|
|
416
807
|
if (name === 'list') {
|
|
417
808
|
if (input.length)
|
|
418
809
|
throw new CliError('USAGE', USAGE_LINES);
|
|
@@ -428,10 +819,18 @@ async function recipeCommand(input) {
|
|
|
428
819
|
'--db': 'db',
|
|
429
820
|
'--dev-infra': 'devInfra',
|
|
430
821
|
'--admin': 'admin',
|
|
822
|
+
'--mail': 'mail',
|
|
823
|
+
'--apps': 'apps',
|
|
824
|
+
'--domains': 'domains',
|
|
825
|
+
'--realms': 'realms',
|
|
826
|
+
'--worker': 'worker',
|
|
431
827
|
};
|
|
432
828
|
let dir;
|
|
433
829
|
let defaults = false;
|
|
434
830
|
let answersFrom;
|
|
831
|
+
// D-0095: the given answers the agent chose itself, and its one-line reasons.
|
|
832
|
+
let aiChose;
|
|
833
|
+
const aiReasons = new Map();
|
|
435
834
|
let dryRun = false;
|
|
436
835
|
let port;
|
|
437
836
|
let dbPort;
|
|
@@ -467,6 +866,21 @@ async function recipeCommand(input) {
|
|
|
467
866
|
throw new CliError('USAGE', `--answers-from must be user (the user answered) or defaults (the user said to decide)\n${USAGE_LINES}`);
|
|
468
867
|
answersFrom = value;
|
|
469
868
|
}
|
|
869
|
+
else if (arg === '--ai-chose' && aiChose === undefined) {
|
|
870
|
+
const value = input[++index];
|
|
871
|
+
const ids = value?.split(',').map((item) => item.trim());
|
|
872
|
+
if (!value || value.startsWith('-') || !ids?.every(Boolean))
|
|
873
|
+
throw new CliError('USAGE', `--ai-chose takes the comma-separated question ids you chose (for example apps,worker)\n${USAGE_LINES}`);
|
|
874
|
+
aiChose = [...new Set(ids)];
|
|
875
|
+
}
|
|
876
|
+
else if (arg === '--ai-reason') {
|
|
877
|
+
const value = input[++index];
|
|
878
|
+
const at = value?.indexOf('=') ?? -1;
|
|
879
|
+
const id = at > 0 ? value.slice(0, at).trim() : '';
|
|
880
|
+
if (!value || value.startsWith('--') || !id || aiReasons.has(id))
|
|
881
|
+
throw new CliError('USAGE', `--ai-reason takes <id>=<why you chose it>, once per question id\n${USAGE_LINES}`);
|
|
882
|
+
aiReasons.set(id, value.slice(at + 1));
|
|
883
|
+
}
|
|
470
884
|
else if (arg === '--dry-run' && !dryRun)
|
|
471
885
|
dryRun = true;
|
|
472
886
|
else
|
|
@@ -478,11 +892,13 @@ async function recipeCommand(input) {
|
|
|
478
892
|
answers,
|
|
479
893
|
defaults,
|
|
480
894
|
...(answersFrom !== undefined ? { answersFrom } : {}),
|
|
895
|
+
...(aiChose !== undefined ? { aiChose } : {}),
|
|
896
|
+
...(aiReasons.size ? { aiReasons: Object.fromEntries(aiReasons) } : {}),
|
|
481
897
|
dryRun,
|
|
482
898
|
...(dir !== undefined ? { dir } : {}),
|
|
483
899
|
...(port !== undefined ? { port } : {}),
|
|
484
900
|
...(dbPort !== undefined ? { dbPort } : {}),
|
|
485
|
-
},
|
|
901
|
+
}, await newProject());
|
|
486
902
|
if (result.status === 'needs-answers') {
|
|
487
903
|
// Not an error of the command: the answers come from the user. Nothing was installed.
|
|
488
904
|
process.exitCode = 2;
|
|
@@ -517,6 +933,72 @@ async function recipeCommand(input) {
|
|
|
517
933
|
* Reads `--name <number>` style flags of the server commands; anything else is a usage error.
|
|
518
934
|
* `numbers` maps each flag to its range (default 1 to 3600000).
|
|
519
935
|
*/
|
|
936
|
+
/** `dokebi deploy init|proxy` (D-0184). */
|
|
937
|
+
async function deployCommand(input) {
|
|
938
|
+
const action = input.shift();
|
|
939
|
+
if (action !== 'init' && action !== 'proxy')
|
|
940
|
+
throw action && !action.startsWith('-')
|
|
941
|
+
? unknownCommand(action, ['init', 'proxy'], 'deploy ')
|
|
942
|
+
: new CliError('USAGE', USAGE_LINES);
|
|
943
|
+
const instances = [];
|
|
944
|
+
const domains = [];
|
|
945
|
+
let target;
|
|
946
|
+
let port;
|
|
947
|
+
let proxy;
|
|
948
|
+
let dryRun = false;
|
|
949
|
+
const value = (index) => {
|
|
950
|
+
const text = input[index];
|
|
951
|
+
if (text === undefined || text.startsWith('-'))
|
|
952
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
953
|
+
return text;
|
|
954
|
+
};
|
|
955
|
+
for (let index = 0; index < input.length; index++) {
|
|
956
|
+
const arg = input[index];
|
|
957
|
+
if (arg === '--dry-run' && !dryRun)
|
|
958
|
+
dryRun = true;
|
|
959
|
+
else if (arg === '--instance')
|
|
960
|
+
instances.push(value(++index));
|
|
961
|
+
else if (action === 'init' && arg === '--target' && target === undefined)
|
|
962
|
+
target = value(++index);
|
|
963
|
+
else if (action === 'init' && arg === '--port' && port === undefined) {
|
|
964
|
+
const number = Number(value(++index));
|
|
965
|
+
if (!Number.isSafeInteger(number) || number < 1024 || number > 65535)
|
|
966
|
+
throw new CliError('USAGE', `--port must be an integer from 1024 to 65535\n${USAGE_LINES}`);
|
|
967
|
+
port = number;
|
|
968
|
+
}
|
|
969
|
+
else if (action === 'proxy' && (arg === '--caddy' || arg === '--nginx') && !proxy)
|
|
970
|
+
proxy = arg === '--caddy' ? 'caddy' : 'nginx';
|
|
971
|
+
else if (action === 'proxy' && arg === '--domain')
|
|
972
|
+
domains.push(value(++index));
|
|
973
|
+
else
|
|
974
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
975
|
+
}
|
|
976
|
+
if (target !== undefined && target !== 'systemd' && target !== 'compose')
|
|
977
|
+
throw new CliError('USAGE', `--target is systemd or compose\n${USAGE_LINES}`);
|
|
978
|
+
const deploy = await import('./deploy.js');
|
|
979
|
+
mutationStarted = !dryRun;
|
|
980
|
+
if (action === 'init') {
|
|
981
|
+
const result = await deploy.deployInit(process.cwd(), {
|
|
982
|
+
target: (target ?? 'systemd'),
|
|
983
|
+
instances: instances.length ? instances : ['production'],
|
|
984
|
+
...(port === undefined ? {} : { port }),
|
|
985
|
+
dryRun,
|
|
986
|
+
});
|
|
987
|
+
printWarnings(result.warnings);
|
|
988
|
+
print('deploy init', result);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (!proxy || !domains.length || instances.length > 1)
|
|
992
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
993
|
+
const result = await deploy.deployProxy(process.cwd(), {
|
|
994
|
+
proxy,
|
|
995
|
+
domains,
|
|
996
|
+
instance: instances[0] ?? 'production',
|
|
997
|
+
dryRun,
|
|
998
|
+
});
|
|
999
|
+
printWarnings(result.warnings);
|
|
1000
|
+
print('deploy proxy', result);
|
|
1001
|
+
}
|
|
520
1002
|
function serverFlags(input, numbers, switches) {
|
|
521
1003
|
const result = { numbers: new Map(), switches: new Set() };
|
|
522
1004
|
for (let index = 0; index < input.length; index++) {
|
|
@@ -536,9 +1018,10 @@ function serverFlags(input, numbers, switches) {
|
|
|
536
1018
|
}
|
|
537
1019
|
return result;
|
|
538
1020
|
}
|
|
539
|
-
/** `dokebi dev|start|stop|status|logs` (D-0044): the project's process. */
|
|
1021
|
+
/** `dokebi dev|start|stop|reload|status|logs` (D-0044, D-0154): the project's process. */
|
|
540
1022
|
async function serverCommand(input) {
|
|
541
1023
|
const root = process.cwd();
|
|
1024
|
+
const server = await import('./server.js');
|
|
542
1025
|
const print = (result, text) => process.stdout.write(machine ? JSON.stringify(envelope(command, result)) + '\n' : text + '\n');
|
|
543
1026
|
if (command === 'dev') {
|
|
544
1027
|
if (input.length || machine)
|
|
@@ -584,6 +1067,23 @@ async function serverCommand(input) {
|
|
|
584
1067
|
? `not running (pid ${result.pid} had already exited; removed the pid file)`
|
|
585
1068
|
: 'not running');
|
|
586
1069
|
}
|
|
1070
|
+
else if (command === 'reload') {
|
|
1071
|
+
const options = serverFlags(input, { '--timeout-ms': [1000, 3_600_000] }, []);
|
|
1072
|
+
const result = await server.reload(root, options.numbers.get('--timeout-ms'));
|
|
1073
|
+
if (!machine)
|
|
1074
|
+
for (const warning of result.warnings ?? [])
|
|
1075
|
+
process.stderr.write(`warning: ${warning}\n`);
|
|
1076
|
+
// Not running is the answer of status (exit 3), not an error.
|
|
1077
|
+
if (!result.running)
|
|
1078
|
+
process.exitCode = 3;
|
|
1079
|
+
print(result, !result.running
|
|
1080
|
+
? 'not running (npx dokebi start)'
|
|
1081
|
+
: result.outcome === 'applied'
|
|
1082
|
+
? `reloaded pid ${result.pid} in ${result.ms} ms${result.revision !== undefined ? ` (revision ${result.revision})` : ''}${result.paths?.length ? `: ${result.paths.join(', ')}` : ''}`
|
|
1083
|
+
: result.outcome === 'unchanged'
|
|
1084
|
+
? `pid ${result.pid} read dokebi.yaml again: nothing changed`
|
|
1085
|
+
: `signalled pid ${result.pid}; its outcome is not in the log yet (npx dokebi logs)`);
|
|
1086
|
+
}
|
|
587
1087
|
else if (command === 'status') {
|
|
588
1088
|
if (input.length)
|
|
589
1089
|
throw new CliError('USAGE', USAGE_LINES);
|
|
@@ -617,7 +1117,7 @@ async function serverCommand(input) {
|
|
|
617
1117
|
/** `dokebi app add|remove|list` (D-0030): apps of @dokebi/app and their domains. */
|
|
618
1118
|
async function app(input) {
|
|
619
1119
|
const action = input.shift();
|
|
620
|
-
const project =
|
|
1120
|
+
const project = await newProject();
|
|
621
1121
|
let result;
|
|
622
1122
|
if (action === 'list') {
|
|
623
1123
|
if (input.length)
|
|
@@ -631,17 +1131,47 @@ async function app(input) {
|
|
|
631
1131
|
let dryRun = false;
|
|
632
1132
|
let inertia = false;
|
|
633
1133
|
let ssr = false;
|
|
1134
|
+
let seal = false;
|
|
634
1135
|
let template;
|
|
1136
|
+
/** `--view ejs|none` (M50): EJS pages, or no views (a JSON API). */
|
|
1137
|
+
let view;
|
|
635
1138
|
const domains = [];
|
|
1139
|
+
// D-0098: who chose the view of this app (the same flags as the recipe's, D-0095).
|
|
1140
|
+
let answersFrom;
|
|
1141
|
+
let aiChose;
|
|
1142
|
+
const aiReasons = new Map();
|
|
636
1143
|
for (let index = 0; index < input.length; index++) {
|
|
637
1144
|
const arg = input[index];
|
|
638
1145
|
const value = input[index + 1];
|
|
639
1146
|
if (arg === '--dry-run' && !dryRun)
|
|
640
1147
|
dryRun = true;
|
|
1148
|
+
else if (action === 'add' && arg === '--answers-from' && answersFrom === undefined) {
|
|
1149
|
+
if (value !== 'user' && value !== 'defaults')
|
|
1150
|
+
throw new CliError('USAGE', `--answers-from must be user (the user chose the view) or defaults (the user left it to you)\n${USAGE_LINES}`);
|
|
1151
|
+
answersFrom = value;
|
|
1152
|
+
index++;
|
|
1153
|
+
}
|
|
1154
|
+
else if (action === 'add' && arg === '--ai-chose' && aiChose === undefined) {
|
|
1155
|
+
const ids = value?.split(',').map((item) => item.trim());
|
|
1156
|
+
if (!value || value.startsWith('-') || !ids?.every(Boolean))
|
|
1157
|
+
throw new CliError('USAGE', `--ai-chose takes view on app add\n${USAGE_LINES}`);
|
|
1158
|
+
aiChose = [...new Set(ids)];
|
|
1159
|
+
index++;
|
|
1160
|
+
}
|
|
1161
|
+
else if (action === 'add' && arg === '--ai-reason') {
|
|
1162
|
+
const at = value?.indexOf('=') ?? -1;
|
|
1163
|
+
const id = at > 0 ? value.slice(0, at).trim() : '';
|
|
1164
|
+
if (!value || value.startsWith('--') || !id || aiReasons.has(id))
|
|
1165
|
+
throw new CliError('USAGE', `--ai-reason takes view=<why you chose it>, once\n${USAGE_LINES}`);
|
|
1166
|
+
aiReasons.set(id, value.slice(at + 1));
|
|
1167
|
+
index++;
|
|
1168
|
+
}
|
|
641
1169
|
else if (action === 'add' && arg === '--inertia' && !inertia)
|
|
642
1170
|
inertia = true;
|
|
643
1171
|
else if (action === 'add' && arg === '--ssr' && !ssr)
|
|
644
1172
|
ssr = true;
|
|
1173
|
+
else if (action === 'add' && arg === '--seal' && !seal)
|
|
1174
|
+
seal = true;
|
|
645
1175
|
else if (action === 'add' && arg === '--domain' && value && !value.startsWith('-')) {
|
|
646
1176
|
domains.push(value);
|
|
647
1177
|
index++;
|
|
@@ -650,22 +1180,42 @@ async function app(input) {
|
|
|
650
1180
|
template = value;
|
|
651
1181
|
index++;
|
|
652
1182
|
}
|
|
1183
|
+
else if (action === 'add' && arg === '--view' && view === undefined) {
|
|
1184
|
+
if (value !== 'ejs' && value !== 'none')
|
|
1185
|
+
throw new CliError('USAGE', `--view takes ejs (EJS pages) or none (no views, a JSON API); an Inertia app is --inertia\n${USAGE_LINES}`);
|
|
1186
|
+
view = value;
|
|
1187
|
+
index++;
|
|
1188
|
+
}
|
|
653
1189
|
else
|
|
654
1190
|
throw new CliError('USAGE', USAGE_LINES);
|
|
655
1191
|
}
|
|
656
1192
|
// Server rendering (D-0039) is a choice of an Inertia app.
|
|
657
1193
|
if (ssr && !inertia)
|
|
658
1194
|
throw new CliError('USAGE', USAGE_LINES);
|
|
1195
|
+
// One flag chooses the view; an app without views has no template.
|
|
1196
|
+
if (view !== undefined && inertia)
|
|
1197
|
+
throw new CliError('USAGE', `--view and --inertia both choose the view: give one\n${USAGE_LINES}`);
|
|
1198
|
+
if (view === 'none' && template !== undefined)
|
|
1199
|
+
throw new CliError('USAGE', `--view none makes an app without views: it takes no --template\n${USAGE_LINES}`);
|
|
1200
|
+
const viewSource = {
|
|
1201
|
+
...(answersFrom !== undefined ? { answersFrom } : {}),
|
|
1202
|
+
...(aiChose !== undefined ? { aiChose } : {}),
|
|
1203
|
+
...(aiReasons.size ? { aiReasons: Object.fromEntries(aiReasons) } : {}),
|
|
1204
|
+
};
|
|
1205
|
+
const views = action === 'add' ? await import('./view-choice.js') : undefined;
|
|
1206
|
+
views?.checkViewSource(viewSource);
|
|
659
1207
|
mutationStarted = true;
|
|
660
1208
|
result =
|
|
661
|
-
action === 'add'
|
|
662
|
-
? await project.appAdd(process.cwd(), name, {
|
|
1209
|
+
action === 'add' && views
|
|
1210
|
+
? await views.recordAppView(process.cwd(), await project.appAdd(process.cwd(), name, {
|
|
663
1211
|
domains,
|
|
664
1212
|
dryRun,
|
|
665
1213
|
inertia,
|
|
666
1214
|
ssr,
|
|
1215
|
+
seal,
|
|
667
1216
|
...(template ? { template } : {}),
|
|
668
|
-
|
|
1217
|
+
...(view ? { view } : {}),
|
|
1218
|
+
}), name, views.appViewValue(inertia, ssr, view === 'none'), viewSource, dryRun)
|
|
669
1219
|
: await project.appRemove(process.cwd(), name, dryRun);
|
|
670
1220
|
}
|
|
671
1221
|
else if (action === 'link' || action === 'unlink') {
|
|
@@ -674,12 +1224,15 @@ async function app(input) {
|
|
|
674
1224
|
if (!first || first.startsWith('-') || !domain || domain.startsWith('-'))
|
|
675
1225
|
throw new CliError('USAGE', USAGE_LINES);
|
|
676
1226
|
let dryRun = false;
|
|
1227
|
+
let seal = false;
|
|
677
1228
|
let template;
|
|
678
1229
|
for (let index = 0; index < input.length; index++) {
|
|
679
1230
|
const arg = input[index];
|
|
680
1231
|
const value = input[index + 1];
|
|
681
1232
|
if (arg === '--dry-run' && !dryRun)
|
|
682
1233
|
dryRun = true;
|
|
1234
|
+
else if (action === 'link' && arg === '--seal' && !seal)
|
|
1235
|
+
seal = true;
|
|
683
1236
|
else if (action === 'link' && arg === '--template' && value && !template) {
|
|
684
1237
|
template = value;
|
|
685
1238
|
index++;
|
|
@@ -692,6 +1245,7 @@ async function app(input) {
|
|
|
692
1245
|
action === 'link'
|
|
693
1246
|
? await project.appLink(process.cwd(), first, domain, {
|
|
694
1247
|
dryRun,
|
|
1248
|
+
seal,
|
|
695
1249
|
...(template ? { template } : {}),
|
|
696
1250
|
})
|
|
697
1251
|
: await project.appUnlink(process.cwd(), domain, dryRun);
|
|
@@ -705,7 +1259,7 @@ async function app(input) {
|
|
|
705
1259
|
/** `dokebi module add|remove|list` (D-0032): feature modules in src/shared/modules/<name>. */
|
|
706
1260
|
async function moduleCommand(input) {
|
|
707
1261
|
const action = input.shift();
|
|
708
|
-
const project =
|
|
1262
|
+
const project = await newProject();
|
|
709
1263
|
let result;
|
|
710
1264
|
if (action === 'list') {
|
|
711
1265
|
if (input.length)
|
|
@@ -738,6 +1292,50 @@ async function moduleCommand(input) {
|
|
|
738
1292
|
throw new CliError('USAGE', USAGE_LINES);
|
|
739
1293
|
process.stdout.write(JSON.stringify(machine ? envelope(`module ${action}`, result) : result, null, 2) + '\n');
|
|
740
1294
|
}
|
|
1295
|
+
/** `dokebi settings add|sync|remove|list` (D-0155): the settings sections of the application. */
|
|
1296
|
+
async function settingsCommand(input) {
|
|
1297
|
+
const action = input.shift();
|
|
1298
|
+
const project = await newProject();
|
|
1299
|
+
let result;
|
|
1300
|
+
if (action === 'list') {
|
|
1301
|
+
if (input.length)
|
|
1302
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1303
|
+
result = await project.settingsList(process.cwd());
|
|
1304
|
+
}
|
|
1305
|
+
else if (action === 'sync') {
|
|
1306
|
+
let dryRun = false;
|
|
1307
|
+
for (const arg of input) {
|
|
1308
|
+
if (arg === '--dry-run' && !dryRun)
|
|
1309
|
+
dryRun = true;
|
|
1310
|
+
else
|
|
1311
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1312
|
+
}
|
|
1313
|
+
mutationStarted = !dryRun;
|
|
1314
|
+
result = await project.settingsSync(process.cwd(), dryRun);
|
|
1315
|
+
}
|
|
1316
|
+
else if (action === 'add' || action === 'remove') {
|
|
1317
|
+
const section = input.shift();
|
|
1318
|
+
if (!section || section.startsWith('-'))
|
|
1319
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1320
|
+
let dryRun = false;
|
|
1321
|
+
for (const arg of input) {
|
|
1322
|
+
if (arg === '--dry-run' && !dryRun)
|
|
1323
|
+
dryRun = true;
|
|
1324
|
+
else
|
|
1325
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1326
|
+
}
|
|
1327
|
+
mutationStarted = !dryRun;
|
|
1328
|
+
result =
|
|
1329
|
+
action === 'add'
|
|
1330
|
+
? await project.settingsAdd(process.cwd(), section, { dryRun })
|
|
1331
|
+
: await project.settingsRemove(process.cwd(), section, dryRun);
|
|
1332
|
+
}
|
|
1333
|
+
else if (action && !action.startsWith('-'))
|
|
1334
|
+
throw unknownCommand(action, ['add', 'sync', 'remove', 'list'], 'settings ');
|
|
1335
|
+
else
|
|
1336
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1337
|
+
process.stdout.write(JSON.stringify(machine ? envelope(`settings ${action}`, result) : result, null, 2) + '\n');
|
|
1338
|
+
}
|
|
741
1339
|
await main().catch((error) => {
|
|
742
1340
|
if (machine)
|
|
743
1341
|
process.stdout.write(JSON.stringify(failure(command, error, mutationStarted)) + '\n');
|
|
@@ -788,6 +1386,9 @@ function nextSteps(error) {
|
|
|
788
1386
|
const hint = error.details.hint;
|
|
789
1387
|
if (typeof hint === 'string')
|
|
790
1388
|
lines.add(`hint: ${hint}`);
|
|
1389
|
+
const fix = error.details.fix;
|
|
1390
|
+
if (typeof fix === 'string')
|
|
1391
|
+
lines.add(`fix: ${fix}`);
|
|
791
1392
|
// A decision that belongs to the user (PREFLIGHT_FAILED with a taken port, AI-M03).
|
|
792
1393
|
const { question, next, waitForUser } = error.details;
|
|
793
1394
|
if (typeof question === 'string')
|