@dokebi/cli 0.4.2 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +518 -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 +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/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 +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/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,120 @@ 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
|
+
}
|
|
175
306
|
if (command === 'verify') {
|
|
176
|
-
const options = serverFlags(args, { '--wait-ms': undefined }, []);
|
|
307
|
+
const options = serverFlags(args, { '--wait-ms': undefined, '--test-timeout-ms': [1000, 3_600_000] }, []);
|
|
177
308
|
const waitMs = options.numbers.get('--wait-ms');
|
|
178
|
-
const
|
|
309
|
+
const testTimeoutMs = options.numbers.get('--test-timeout-ms');
|
|
310
|
+
const [{ verify }, { testLines }, { TELL_USER_RELAY }] = await Promise.all([
|
|
311
|
+
import('./verify.js'),
|
|
312
|
+
import('./verify-tests.js'),
|
|
313
|
+
import('./recipe-record.js'),
|
|
314
|
+
]);
|
|
315
|
+
const result = await verify(process.cwd(), {
|
|
316
|
+
...(waitMs !== undefined ? { waitMs } : {}),
|
|
317
|
+
...(testTimeoutMs !== undefined ? { testTimeoutMs } : {}),
|
|
318
|
+
});
|
|
179
319
|
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
|
-
|
|
320
|
+
.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) : ''}`)
|
|
321
|
+
.join('\n')}\n${result.ok ? 'verify ok' : 'verify FAILED'}${addressLines(result)}${(result.warnings ?? [])
|
|
322
|
+
.map((warning) => `\nwarning: ${warning.code} (${warning.path}): ${warning.hint}`)
|
|
323
|
+
.join('')}${
|
|
324
|
+
// A taken port is the user's call (AI-M04 F5), as in the recipe preflight.
|
|
325
|
+
result.question
|
|
326
|
+
? `\n${result.waitForUser ? 'ask the user' : 'question'}: ${result.question}\nnext: ${result.next}`
|
|
327
|
+
: ''}${result.tellUser
|
|
328
|
+
? `\nTell the user exactly these lines. ${TELL_USER_RELAY.instruction}\n${result.tellUser.map((line) => `- ${line}`).join('\n')}`
|
|
329
|
+
: ''}${
|
|
330
|
+
// D-0105 §4: what the agent writes into its final message itself (not copied).
|
|
331
|
+
result.finalReport
|
|
332
|
+
? `\nYour final message must also contain:\n${result.finalReport.map((line) => `- ${line}`).join('\n')}`
|
|
183
333
|
: ''}`, result.ok);
|
|
184
334
|
if (!result.ok)
|
|
185
335
|
process.exitCode = 1;
|
|
186
336
|
return;
|
|
187
337
|
}
|
|
338
|
+
if (command === 'describe') {
|
|
339
|
+
// Read only (D-0077 B): the app's setup in describe mode; nothing listens or connects.
|
|
340
|
+
// D-0077 C: --openapi [--app <name>] prints OpenAPI 3.1 documents instead.
|
|
341
|
+
const appAt = args.indexOf('--app');
|
|
342
|
+
const appName = appAt < 0 ? undefined : args[appAt + 1];
|
|
343
|
+
if (appAt >= 0 && (appName === undefined || appName.startsWith('-')))
|
|
344
|
+
throw new CliError('USAGE', `--app needs an app name\n${USAGE_LINES}`);
|
|
345
|
+
const rest = appAt < 0 ? args : args.filter((_, index) => index !== appAt && index !== appAt + 1);
|
|
346
|
+
const options = serverFlags(rest, { '--timeout-ms': [1000, 600_000] }, [
|
|
347
|
+
'--with-db',
|
|
348
|
+
'--openapi',
|
|
349
|
+
]);
|
|
350
|
+
if (appName !== undefined && !options.switches.has('--openapi'))
|
|
351
|
+
throw new CliError('USAGE', `--app is for --openapi\n${USAGE_LINES}`);
|
|
352
|
+
const timeoutMs = options.numbers.get('--timeout-ms');
|
|
353
|
+
const { describeProject, describeText } = await import('./describe.js');
|
|
354
|
+
const result = await describeProject(process.cwd(), {
|
|
355
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
356
|
+
// D-0077 F: --with-db also reads the tables (a separate step that connects).
|
|
357
|
+
withDb: options.switches.has('--with-db'),
|
|
358
|
+
});
|
|
359
|
+
if (options.switches.has('--openapi')) {
|
|
360
|
+
const { openApi, projectVersion } = await import('./openapi.js');
|
|
361
|
+
const version = projectVersion(process.cwd());
|
|
362
|
+
const documents = openApi(result, {
|
|
363
|
+
...(appName === undefined ? {} : { app: appName }),
|
|
364
|
+
...(version === undefined ? {} : { version }),
|
|
365
|
+
});
|
|
366
|
+
print(command, documents, JSON.stringify(documents, null, 2));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
print(command, result, describeText(result));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (command === 'drift') {
|
|
373
|
+
// D-0120: describe --with-db (it connects) against an expected file of the describe shape.
|
|
374
|
+
const at = args.indexOf('--expected');
|
|
375
|
+
const expected = at < 0 ? undefined : args[at + 1];
|
|
376
|
+
if (expected === undefined || expected.startsWith('-'))
|
|
377
|
+
throw new CliError('USAGE', `--expected needs a file\n${USAGE_LINES}`);
|
|
378
|
+
const options = serverFlags(args.filter((_, index) => index !== at && index !== at + 1), { '--timeout-ms': [1000, 600_000] }, []);
|
|
379
|
+
const timeoutMs = options.numbers.get('--timeout-ms');
|
|
380
|
+
const { drift, driftText } = await import('./drift.js');
|
|
381
|
+
const result = await drift(process.cwd(), {
|
|
382
|
+
expected,
|
|
383
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
384
|
+
});
|
|
385
|
+
print(command, result, driftText(result), result.ok);
|
|
386
|
+
if (!result.ok)
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
188
390
|
if (command === 'check') {
|
|
189
391
|
if (args.length)
|
|
190
392
|
throw new CliError('USAGE', USAGE_LINES);
|
|
393
|
+
const { check, checkText } = await import('./check/index.js');
|
|
191
394
|
const result = await check(process.cwd());
|
|
192
395
|
print(command, result, checkText(result), result.ok);
|
|
193
396
|
if (!result.ok)
|
|
@@ -196,23 +399,128 @@ async function main() {
|
|
|
196
399
|
}
|
|
197
400
|
if (command === 'config') {
|
|
198
401
|
const action = args.shift();
|
|
402
|
+
const parseSetOption = await setParser();
|
|
403
|
+
if (action === 'show') {
|
|
404
|
+
// Read only: secrets are hidden in every mode (config-show.ts).
|
|
405
|
+
if (args.length > 1 || args[0]?.startsWith('-'))
|
|
406
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
407
|
+
const target = args[0];
|
|
408
|
+
const dot = target?.indexOf('.') ?? -1;
|
|
409
|
+
if (target !== undefined && (dot === 0 || target.includes('=')))
|
|
410
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
411
|
+
const section = target === undefined ? undefined : dot > 0 ? target.slice(0, dot) : target;
|
|
412
|
+
// The path is checked like a --set path (segments, no prototype keys).
|
|
413
|
+
const path = target && dot > 0 ? parseSetOption(`${target.slice(dot + 1)}=0`).path : [];
|
|
414
|
+
const { configShow, configShowText } = await import('./config-show.js');
|
|
415
|
+
const result = await configShow(process.cwd(), section === undefined ? undefined : { section, path });
|
|
416
|
+
print('config show', result, configShowText(result));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (action === 'diff') {
|
|
420
|
+
// Read only (AI-M45): option paths of dokebi.yaml against dokebi.example.yaml, no values.
|
|
421
|
+
// --write (AI-M46) copies the paths only in dokebi.yaml into the sample, without secrets.
|
|
422
|
+
let write = false;
|
|
423
|
+
let dryRun = false;
|
|
424
|
+
let prune = false;
|
|
425
|
+
let values = false;
|
|
426
|
+
const rest = [];
|
|
427
|
+
for (const arg of args) {
|
|
428
|
+
if (arg === '--write' && !write)
|
|
429
|
+
write = true;
|
|
430
|
+
else if (arg === '--dry-run' && !dryRun)
|
|
431
|
+
dryRun = true;
|
|
432
|
+
else if (arg === '--prune' && !prune)
|
|
433
|
+
prune = true;
|
|
434
|
+
else if (arg === '--values' && !values)
|
|
435
|
+
values = true;
|
|
436
|
+
else
|
|
437
|
+
rest.push(arg);
|
|
438
|
+
}
|
|
439
|
+
if ((dryRun || prune || values) && !write)
|
|
440
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
441
|
+
if (rest.length > 1 || rest[0]?.startsWith('-') || rest[0]?.includes('.'))
|
|
442
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
443
|
+
const { configDiff, configDiffText } = await import('./config-diff.js');
|
|
444
|
+
if (write) {
|
|
445
|
+
mutationStarted = true;
|
|
446
|
+
const result = await (await newProject()).configSample(process.cwd(), rest[0], dryRun, prune, values);
|
|
447
|
+
const listed = (label, paths) => paths.length
|
|
448
|
+
? `${label} [${paths.length}]:\n${paths.map((path) => ` ${path}`).join('\n')}\n`
|
|
449
|
+
: '';
|
|
450
|
+
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)}`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const result = await configDiff(process.cwd(), rest[0]);
|
|
454
|
+
print('config diff', result, configDiffText(result));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (action === 'init') {
|
|
458
|
+
// D-0163: a fresh copy of the project (a clone) has no dokebi.yaml; written from the sample.
|
|
459
|
+
const dryRun = args.includes('--dry-run');
|
|
460
|
+
if (args.some((arg) => arg !== '--dry-run'))
|
|
461
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
462
|
+
mutationStarted = true;
|
|
463
|
+
const result = await (await newProject()).configInit(process.cwd(), dryRun);
|
|
464
|
+
print('config init', result);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
199
467
|
if (action === 'secret') {
|
|
200
468
|
const dryRun = args.includes('--dry-run');
|
|
201
|
-
const
|
|
469
|
+
const rotate = args.includes('--rotate');
|
|
470
|
+
const fromStdin = args.includes('--stdin');
|
|
471
|
+
const rest = args.filter((arg) => arg !== '--dry-run' && arg !== '--rotate' && arg !== '--stdin');
|
|
202
472
|
const target = rest[0];
|
|
203
473
|
const dot = target?.indexOf('.') ?? -1;
|
|
204
474
|
if (rest.length !== 1 || !target || dot <= 0 || target.includes('='))
|
|
205
475
|
throw new CliError('USAGE', USAGE_LINES);
|
|
206
476
|
// The path is checked like a --set path (segments, no prototype keys).
|
|
207
477
|
const path = parseSetOption(`${target.slice(dot + 1)}=0`).path;
|
|
478
|
+
// --stdin: a secret someone else issued (a log receiver's token), never on the command line.
|
|
479
|
+
const value = fromStdin ? await readSecretInput() : undefined;
|
|
208
480
|
mutationStarted = true;
|
|
209
|
-
const result = await
|
|
481
|
+
const result = await (await newProject()).configSecret(process.cwd(), target.slice(0, dot), path, dryRun, rotate, value);
|
|
210
482
|
print('config secret', result);
|
|
211
483
|
return;
|
|
212
484
|
}
|
|
485
|
+
if (action === 'unset') {
|
|
486
|
+
// D-0153: <plugin-id>.<path>...; never a value (no `=`). A section alone (no path) only
|
|
487
|
+
// for one no installed plugin reads (an old section left in the files).
|
|
488
|
+
let dryRun = false;
|
|
489
|
+
const targets = [];
|
|
490
|
+
for (const arg of args) {
|
|
491
|
+
if (arg === '--dry-run' && !dryRun)
|
|
492
|
+
dryRun = true;
|
|
493
|
+
else if (!arg.startsWith('-')) {
|
|
494
|
+
if (arg.includes('='))
|
|
495
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
496
|
+
const dot = arg.indexOf('.');
|
|
497
|
+
if (dot === 0)
|
|
498
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
499
|
+
// The path is checked like a --set path (segments, no prototype keys).
|
|
500
|
+
if (dot < 0) {
|
|
501
|
+
parseSetOption(`${arg}=0`);
|
|
502
|
+
targets.push({ plugin: arg, path: [] });
|
|
503
|
+
}
|
|
504
|
+
else
|
|
505
|
+
targets.push({
|
|
506
|
+
plugin: arg.slice(0, dot),
|
|
507
|
+
path: parseSetOption(`${arg.slice(dot + 1)}=0`).path,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
else
|
|
511
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
512
|
+
}
|
|
513
|
+
if (!targets.length)
|
|
514
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
515
|
+
mutationStarted = true;
|
|
516
|
+
const result = await (await newProject()).configUnset(process.cwd(), targets, dryRun);
|
|
517
|
+
printWarnings(result.warnings);
|
|
518
|
+
print('config unset', result);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
213
521
|
if (action !== 'set') {
|
|
214
522
|
if (action && !action.startsWith('-'))
|
|
215
|
-
throw unknownCommand(action, ['set', 'secret'], 'config ');
|
|
523
|
+
throw unknownCommand(action, ['set', 'secret', 'show', 'unset'], 'config ');
|
|
216
524
|
throw new CliError('USAGE', USAGE_LINES);
|
|
217
525
|
}
|
|
218
526
|
let dryRun = false;
|
|
@@ -234,17 +542,32 @@ async function main() {
|
|
|
234
542
|
if (!assignments.length)
|
|
235
543
|
throw new CliError('USAGE', USAGE_LINES);
|
|
236
544
|
mutationStarted = true;
|
|
237
|
-
const result = await
|
|
545
|
+
const result = await (await newProject()).configSet(process.cwd(), assignments, dryRun);
|
|
238
546
|
printWarnings(result.warnings);
|
|
239
547
|
print('config set', result);
|
|
240
548
|
return;
|
|
241
549
|
}
|
|
242
550
|
if (command === 'dev-infra') {
|
|
551
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
552
|
+
const action = args[0];
|
|
553
|
+
args.shift();
|
|
554
|
+
const files = [];
|
|
555
|
+
while (args.length) {
|
|
556
|
+
if (args.shift() !== '--file' || !args[0])
|
|
557
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
558
|
+
files.push(args[0]);
|
|
559
|
+
args.shift();
|
|
560
|
+
}
|
|
561
|
+
mutationStarted = action !== 'status';
|
|
562
|
+
const { runDevCompose } = await import('./dev-compose.js');
|
|
563
|
+
print(command, await runDevCompose(process.cwd(), action, files.length ? files : undefined));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
243
566
|
const dryRun = args.includes('--dry-run');
|
|
244
567
|
const options = serverFlags(args.filter((arg) => arg !== '--dry-run'), { '--db-port': [1, 65535] }, []);
|
|
245
568
|
mutationStarted = true;
|
|
246
569
|
const dbPort = options.numbers.get('--db-port');
|
|
247
|
-
const result = await
|
|
570
|
+
const result = await (await newProject()).devInfra(process.cwd(), {
|
|
248
571
|
dryRun,
|
|
249
572
|
...(dbPort !== undefined ? { dbPort } : {}),
|
|
250
573
|
});
|
|
@@ -255,6 +578,7 @@ async function main() {
|
|
|
255
578
|
const verbose = args.length === 1 && args[0] === '--verbose';
|
|
256
579
|
if (args.length && !verbose)
|
|
257
580
|
throw new CliError('USAGE', USAGE_LINES);
|
|
581
|
+
const { ProjectInspector } = await import('./project-inspector.js');
|
|
258
582
|
const result = await new ProjectInspector().inspect(process.cwd(), {
|
|
259
583
|
runChecks: command === 'doctor',
|
|
260
584
|
});
|
|
@@ -273,7 +597,7 @@ async function main() {
|
|
|
273
597
|
if (args.length && (args.length !== 1 || args[0] !== '--dry-run'))
|
|
274
598
|
throw new CliError('USAGE', USAGE_LINES);
|
|
275
599
|
mutationStarted = true;
|
|
276
|
-
const result = await
|
|
600
|
+
const result = await (await newProject()).migrate(process.cwd(), args[0] === '--dry-run');
|
|
277
601
|
process.stdout.write(JSON.stringify(machine ? envelope(command, result) : result, null, 2) + '\n');
|
|
278
602
|
return;
|
|
279
603
|
}
|
|
@@ -281,7 +605,7 @@ async function main() {
|
|
|
281
605
|
await app(args);
|
|
282
606
|
return;
|
|
283
607
|
}
|
|
284
|
-
if (['dev', 'start', 'stop', 'status', 'logs'].includes(command)) {
|
|
608
|
+
if (['dev', 'start', 'stop', 'reload', 'status', 'logs'].includes(command)) {
|
|
285
609
|
await serverCommand(args);
|
|
286
610
|
return;
|
|
287
611
|
}
|
|
@@ -289,14 +613,19 @@ async function main() {
|
|
|
289
613
|
await moduleCommand(args);
|
|
290
614
|
return;
|
|
291
615
|
}
|
|
616
|
+
if (command === 'settings') {
|
|
617
|
+
await settingsCommand(args);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
292
620
|
if (!['init', 'add', 'upgrade', 'remove'].includes(command))
|
|
293
621
|
throw unknownCommand(command, [...TOP_LEVEL, '--help', '--version']);
|
|
294
622
|
const target = args.shift();
|
|
295
623
|
if (!target || target.startsWith('-'))
|
|
296
624
|
throw new CliError('USAGE', USAGE_LINES);
|
|
297
|
-
const project =
|
|
625
|
+
const project = await newProject();
|
|
298
626
|
let result;
|
|
299
627
|
if (command === 'init') {
|
|
628
|
+
let deploy = false;
|
|
300
629
|
let core;
|
|
301
630
|
let dryRun = false;
|
|
302
631
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -306,6 +635,8 @@ async function main() {
|
|
|
306
635
|
if (!core || core.startsWith('-'))
|
|
307
636
|
throw new CliError('USAGE', USAGE_LINES);
|
|
308
637
|
}
|
|
638
|
+
else if (arg === '--deploy' && !deploy)
|
|
639
|
+
deploy = true;
|
|
309
640
|
else if (arg === '--dry-run' && !dryRun)
|
|
310
641
|
dryRun = true;
|
|
311
642
|
else
|
|
@@ -313,12 +644,18 @@ async function main() {
|
|
|
313
644
|
}
|
|
314
645
|
writeBanner(process.stderr, machine);
|
|
315
646
|
mutationStarted = !dryRun;
|
|
316
|
-
result = await project.init(target, core, dryRun);
|
|
647
|
+
result = await project.init(target, core, dryRun, deploy);
|
|
317
648
|
}
|
|
318
649
|
else if (command === 'add') {
|
|
319
|
-
const options = flags(args, { set:
|
|
650
|
+
const options = flags(args, { set: await setParser(), add: true });
|
|
320
651
|
mutationStarted = true;
|
|
321
652
|
result = await project.add(process.cwd(), target, options.dryRun, options.sets, options.add);
|
|
653
|
+
// D-0098: the first app of the apps host is EJS; its view is recorded (chooser unstated).
|
|
654
|
+
const firstApp = result.firstApp;
|
|
655
|
+
if (firstApp) {
|
|
656
|
+
const { recordAppView } = await import('./view-choice.js');
|
|
657
|
+
result = await recordAppView(process.cwd(), result, firstApp, 'ejs', {}, options.dryRun);
|
|
658
|
+
}
|
|
322
659
|
}
|
|
323
660
|
else if (command === 'upgrade') {
|
|
324
661
|
const options = flags(args, { to: true });
|
|
@@ -354,15 +691,16 @@ function printWarnings(warnings) {
|
|
|
354
691
|
if (machine)
|
|
355
692
|
return;
|
|
356
693
|
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')
|
|
694
|
+
if (warning.code === 'THIRD_PARTY_PACKAGE')
|
|
360
695
|
process.stderr.write(`warning: ${warning.path} is not a @dokebi package; install it only with the user's consent\n`);
|
|
696
|
+
else if (warning.code === 'DEPRECATED_PLAN_FIELD')
|
|
697
|
+
process.stderr.write(`warning: ${warning.plugin} uses the deprecated install plan field ${warning.deprecated} (removed in 0.5); upgrade the plugin\n`);
|
|
361
698
|
}
|
|
362
699
|
/** `dokebi plugins list|search|info|doc` (D-0065). */
|
|
363
700
|
async function pluginsCommand(input) {
|
|
364
701
|
const action = input.shift();
|
|
365
702
|
const name = `plugins ${action}`;
|
|
703
|
+
const { pluginsDoc, pluginsInfo, pluginsList, pluginsSearch } = await import('./plugins-command.js');
|
|
366
704
|
if (action === 'list') {
|
|
367
705
|
let compatible = false;
|
|
368
706
|
let online = false;
|
|
@@ -413,6 +751,7 @@ async function recipeCommand(input) {
|
|
|
413
751
|
const name = input.shift();
|
|
414
752
|
if (!name || name.startsWith('-'))
|
|
415
753
|
throw new CliError('USAGE', USAGE_LINES);
|
|
754
|
+
const { listRecipes, runRecipe } = await import('./recipe.js');
|
|
416
755
|
if (name === 'list') {
|
|
417
756
|
if (input.length)
|
|
418
757
|
throw new CliError('USAGE', USAGE_LINES);
|
|
@@ -428,10 +767,18 @@ async function recipeCommand(input) {
|
|
|
428
767
|
'--db': 'db',
|
|
429
768
|
'--dev-infra': 'devInfra',
|
|
430
769
|
'--admin': 'admin',
|
|
770
|
+
'--mail': 'mail',
|
|
771
|
+
'--apps': 'apps',
|
|
772
|
+
'--domains': 'domains',
|
|
773
|
+
'--realms': 'realms',
|
|
774
|
+
'--worker': 'worker',
|
|
431
775
|
};
|
|
432
776
|
let dir;
|
|
433
777
|
let defaults = false;
|
|
434
778
|
let answersFrom;
|
|
779
|
+
// D-0095: the given answers the agent chose itself, and its one-line reasons.
|
|
780
|
+
let aiChose;
|
|
781
|
+
const aiReasons = new Map();
|
|
435
782
|
let dryRun = false;
|
|
436
783
|
let port;
|
|
437
784
|
let dbPort;
|
|
@@ -467,6 +814,21 @@ async function recipeCommand(input) {
|
|
|
467
814
|
throw new CliError('USAGE', `--answers-from must be user (the user answered) or defaults (the user said to decide)\n${USAGE_LINES}`);
|
|
468
815
|
answersFrom = value;
|
|
469
816
|
}
|
|
817
|
+
else if (arg === '--ai-chose' && aiChose === undefined) {
|
|
818
|
+
const value = input[++index];
|
|
819
|
+
const ids = value?.split(',').map((item) => item.trim());
|
|
820
|
+
if (!value || value.startsWith('-') || !ids?.every(Boolean))
|
|
821
|
+
throw new CliError('USAGE', `--ai-chose takes the comma-separated question ids you chose (for example apps,worker)\n${USAGE_LINES}`);
|
|
822
|
+
aiChose = [...new Set(ids)];
|
|
823
|
+
}
|
|
824
|
+
else if (arg === '--ai-reason') {
|
|
825
|
+
const value = input[++index];
|
|
826
|
+
const at = value?.indexOf('=') ?? -1;
|
|
827
|
+
const id = at > 0 ? value.slice(0, at).trim() : '';
|
|
828
|
+
if (!value || value.startsWith('--') || !id || aiReasons.has(id))
|
|
829
|
+
throw new CliError('USAGE', `--ai-reason takes <id>=<why you chose it>, once per question id\n${USAGE_LINES}`);
|
|
830
|
+
aiReasons.set(id, value.slice(at + 1));
|
|
831
|
+
}
|
|
470
832
|
else if (arg === '--dry-run' && !dryRun)
|
|
471
833
|
dryRun = true;
|
|
472
834
|
else
|
|
@@ -478,11 +840,13 @@ async function recipeCommand(input) {
|
|
|
478
840
|
answers,
|
|
479
841
|
defaults,
|
|
480
842
|
...(answersFrom !== undefined ? { answersFrom } : {}),
|
|
843
|
+
...(aiChose !== undefined ? { aiChose } : {}),
|
|
844
|
+
...(aiReasons.size ? { aiReasons: Object.fromEntries(aiReasons) } : {}),
|
|
481
845
|
dryRun,
|
|
482
846
|
...(dir !== undefined ? { dir } : {}),
|
|
483
847
|
...(port !== undefined ? { port } : {}),
|
|
484
848
|
...(dbPort !== undefined ? { dbPort } : {}),
|
|
485
|
-
},
|
|
849
|
+
}, await newProject());
|
|
486
850
|
if (result.status === 'needs-answers') {
|
|
487
851
|
// Not an error of the command: the answers come from the user. Nothing was installed.
|
|
488
852
|
process.exitCode = 2;
|
|
@@ -536,9 +900,10 @@ function serverFlags(input, numbers, switches) {
|
|
|
536
900
|
}
|
|
537
901
|
return result;
|
|
538
902
|
}
|
|
539
|
-
/** `dokebi dev|start|stop|status|logs` (D-0044): the project's process. */
|
|
903
|
+
/** `dokebi dev|start|stop|reload|status|logs` (D-0044, D-0154): the project's process. */
|
|
540
904
|
async function serverCommand(input) {
|
|
541
905
|
const root = process.cwd();
|
|
906
|
+
const server = await import('./server.js');
|
|
542
907
|
const print = (result, text) => process.stdout.write(machine ? JSON.stringify(envelope(command, result)) + '\n' : text + '\n');
|
|
543
908
|
if (command === 'dev') {
|
|
544
909
|
if (input.length || machine)
|
|
@@ -584,6 +949,23 @@ async function serverCommand(input) {
|
|
|
584
949
|
? `not running (pid ${result.pid} had already exited; removed the pid file)`
|
|
585
950
|
: 'not running');
|
|
586
951
|
}
|
|
952
|
+
else if (command === 'reload') {
|
|
953
|
+
const options = serverFlags(input, { '--timeout-ms': [1000, 3_600_000] }, []);
|
|
954
|
+
const result = await server.reload(root, options.numbers.get('--timeout-ms'));
|
|
955
|
+
if (!machine)
|
|
956
|
+
for (const warning of result.warnings ?? [])
|
|
957
|
+
process.stderr.write(`warning: ${warning}\n`);
|
|
958
|
+
// Not running is the answer of status (exit 3), not an error.
|
|
959
|
+
if (!result.running)
|
|
960
|
+
process.exitCode = 3;
|
|
961
|
+
print(result, !result.running
|
|
962
|
+
? 'not running (npx dokebi start)'
|
|
963
|
+
: result.outcome === 'applied'
|
|
964
|
+
? `reloaded pid ${result.pid} in ${result.ms} ms${result.revision !== undefined ? ` (revision ${result.revision})` : ''}${result.paths?.length ? `: ${result.paths.join(', ')}` : ''}`
|
|
965
|
+
: result.outcome === 'unchanged'
|
|
966
|
+
? `pid ${result.pid} read dokebi.yaml again: nothing changed`
|
|
967
|
+
: `signalled pid ${result.pid}; its outcome is not in the log yet (npx dokebi logs)`);
|
|
968
|
+
}
|
|
587
969
|
else if (command === 'status') {
|
|
588
970
|
if (input.length)
|
|
589
971
|
throw new CliError('USAGE', USAGE_LINES);
|
|
@@ -617,7 +999,7 @@ async function serverCommand(input) {
|
|
|
617
999
|
/** `dokebi app add|remove|list` (D-0030): apps of @dokebi/app and their domains. */
|
|
618
1000
|
async function app(input) {
|
|
619
1001
|
const action = input.shift();
|
|
620
|
-
const project =
|
|
1002
|
+
const project = await newProject();
|
|
621
1003
|
let result;
|
|
622
1004
|
if (action === 'list') {
|
|
623
1005
|
if (input.length)
|
|
@@ -631,17 +1013,47 @@ async function app(input) {
|
|
|
631
1013
|
let dryRun = false;
|
|
632
1014
|
let inertia = false;
|
|
633
1015
|
let ssr = false;
|
|
1016
|
+
let seal = false;
|
|
634
1017
|
let template;
|
|
1018
|
+
/** `--view ejs|none` (M50): EJS pages, or no views (a JSON API). */
|
|
1019
|
+
let view;
|
|
635
1020
|
const domains = [];
|
|
1021
|
+
// D-0098: who chose the view of this app (the same flags as the recipe's, D-0095).
|
|
1022
|
+
let answersFrom;
|
|
1023
|
+
let aiChose;
|
|
1024
|
+
const aiReasons = new Map();
|
|
636
1025
|
for (let index = 0; index < input.length; index++) {
|
|
637
1026
|
const arg = input[index];
|
|
638
1027
|
const value = input[index + 1];
|
|
639
1028
|
if (arg === '--dry-run' && !dryRun)
|
|
640
1029
|
dryRun = true;
|
|
1030
|
+
else if (action === 'add' && arg === '--answers-from' && answersFrom === undefined) {
|
|
1031
|
+
if (value !== 'user' && value !== 'defaults')
|
|
1032
|
+
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}`);
|
|
1033
|
+
answersFrom = value;
|
|
1034
|
+
index++;
|
|
1035
|
+
}
|
|
1036
|
+
else if (action === 'add' && arg === '--ai-chose' && aiChose === undefined) {
|
|
1037
|
+
const ids = value?.split(',').map((item) => item.trim());
|
|
1038
|
+
if (!value || value.startsWith('-') || !ids?.every(Boolean))
|
|
1039
|
+
throw new CliError('USAGE', `--ai-chose takes view on app add\n${USAGE_LINES}`);
|
|
1040
|
+
aiChose = [...new Set(ids)];
|
|
1041
|
+
index++;
|
|
1042
|
+
}
|
|
1043
|
+
else if (action === 'add' && arg === '--ai-reason') {
|
|
1044
|
+
const at = value?.indexOf('=') ?? -1;
|
|
1045
|
+
const id = at > 0 ? value.slice(0, at).trim() : '';
|
|
1046
|
+
if (!value || value.startsWith('--') || !id || aiReasons.has(id))
|
|
1047
|
+
throw new CliError('USAGE', `--ai-reason takes view=<why you chose it>, once\n${USAGE_LINES}`);
|
|
1048
|
+
aiReasons.set(id, value.slice(at + 1));
|
|
1049
|
+
index++;
|
|
1050
|
+
}
|
|
641
1051
|
else if (action === 'add' && arg === '--inertia' && !inertia)
|
|
642
1052
|
inertia = true;
|
|
643
1053
|
else if (action === 'add' && arg === '--ssr' && !ssr)
|
|
644
1054
|
ssr = true;
|
|
1055
|
+
else if (action === 'add' && arg === '--seal' && !seal)
|
|
1056
|
+
seal = true;
|
|
645
1057
|
else if (action === 'add' && arg === '--domain' && value && !value.startsWith('-')) {
|
|
646
1058
|
domains.push(value);
|
|
647
1059
|
index++;
|
|
@@ -650,22 +1062,42 @@ async function app(input) {
|
|
|
650
1062
|
template = value;
|
|
651
1063
|
index++;
|
|
652
1064
|
}
|
|
1065
|
+
else if (action === 'add' && arg === '--view' && view === undefined) {
|
|
1066
|
+
if (value !== 'ejs' && value !== 'none')
|
|
1067
|
+
throw new CliError('USAGE', `--view takes ejs (EJS pages) or none (no views, a JSON API); an Inertia app is --inertia\n${USAGE_LINES}`);
|
|
1068
|
+
view = value;
|
|
1069
|
+
index++;
|
|
1070
|
+
}
|
|
653
1071
|
else
|
|
654
1072
|
throw new CliError('USAGE', USAGE_LINES);
|
|
655
1073
|
}
|
|
656
1074
|
// Server rendering (D-0039) is a choice of an Inertia app.
|
|
657
1075
|
if (ssr && !inertia)
|
|
658
1076
|
throw new CliError('USAGE', USAGE_LINES);
|
|
1077
|
+
// One flag chooses the view; an app without views has no template.
|
|
1078
|
+
if (view !== undefined && inertia)
|
|
1079
|
+
throw new CliError('USAGE', `--view and --inertia both choose the view: give one\n${USAGE_LINES}`);
|
|
1080
|
+
if (view === 'none' && template !== undefined)
|
|
1081
|
+
throw new CliError('USAGE', `--view none makes an app without views: it takes no --template\n${USAGE_LINES}`);
|
|
1082
|
+
const viewSource = {
|
|
1083
|
+
...(answersFrom !== undefined ? { answersFrom } : {}),
|
|
1084
|
+
...(aiChose !== undefined ? { aiChose } : {}),
|
|
1085
|
+
...(aiReasons.size ? { aiReasons: Object.fromEntries(aiReasons) } : {}),
|
|
1086
|
+
};
|
|
1087
|
+
const views = action === 'add' ? await import('./view-choice.js') : undefined;
|
|
1088
|
+
views?.checkViewSource(viewSource);
|
|
659
1089
|
mutationStarted = true;
|
|
660
1090
|
result =
|
|
661
|
-
action === 'add'
|
|
662
|
-
? await project.appAdd(process.cwd(), name, {
|
|
1091
|
+
action === 'add' && views
|
|
1092
|
+
? await views.recordAppView(process.cwd(), await project.appAdd(process.cwd(), name, {
|
|
663
1093
|
domains,
|
|
664
1094
|
dryRun,
|
|
665
1095
|
inertia,
|
|
666
1096
|
ssr,
|
|
1097
|
+
seal,
|
|
667
1098
|
...(template ? { template } : {}),
|
|
668
|
-
|
|
1099
|
+
...(view ? { view } : {}),
|
|
1100
|
+
}), name, views.appViewValue(inertia, ssr, view === 'none'), viewSource, dryRun)
|
|
669
1101
|
: await project.appRemove(process.cwd(), name, dryRun);
|
|
670
1102
|
}
|
|
671
1103
|
else if (action === 'link' || action === 'unlink') {
|
|
@@ -674,12 +1106,15 @@ async function app(input) {
|
|
|
674
1106
|
if (!first || first.startsWith('-') || !domain || domain.startsWith('-'))
|
|
675
1107
|
throw new CliError('USAGE', USAGE_LINES);
|
|
676
1108
|
let dryRun = false;
|
|
1109
|
+
let seal = false;
|
|
677
1110
|
let template;
|
|
678
1111
|
for (let index = 0; index < input.length; index++) {
|
|
679
1112
|
const arg = input[index];
|
|
680
1113
|
const value = input[index + 1];
|
|
681
1114
|
if (arg === '--dry-run' && !dryRun)
|
|
682
1115
|
dryRun = true;
|
|
1116
|
+
else if (action === 'link' && arg === '--seal' && !seal)
|
|
1117
|
+
seal = true;
|
|
683
1118
|
else if (action === 'link' && arg === '--template' && value && !template) {
|
|
684
1119
|
template = value;
|
|
685
1120
|
index++;
|
|
@@ -692,6 +1127,7 @@ async function app(input) {
|
|
|
692
1127
|
action === 'link'
|
|
693
1128
|
? await project.appLink(process.cwd(), first, domain, {
|
|
694
1129
|
dryRun,
|
|
1130
|
+
seal,
|
|
695
1131
|
...(template ? { template } : {}),
|
|
696
1132
|
})
|
|
697
1133
|
: await project.appUnlink(process.cwd(), domain, dryRun);
|
|
@@ -705,7 +1141,7 @@ async function app(input) {
|
|
|
705
1141
|
/** `dokebi module add|remove|list` (D-0032): feature modules in src/shared/modules/<name>. */
|
|
706
1142
|
async function moduleCommand(input) {
|
|
707
1143
|
const action = input.shift();
|
|
708
|
-
const project =
|
|
1144
|
+
const project = await newProject();
|
|
709
1145
|
let result;
|
|
710
1146
|
if (action === 'list') {
|
|
711
1147
|
if (input.length)
|
|
@@ -738,6 +1174,50 @@ async function moduleCommand(input) {
|
|
|
738
1174
|
throw new CliError('USAGE', USAGE_LINES);
|
|
739
1175
|
process.stdout.write(JSON.stringify(machine ? envelope(`module ${action}`, result) : result, null, 2) + '\n');
|
|
740
1176
|
}
|
|
1177
|
+
/** `dokebi settings add|sync|remove|list` (D-0155): the settings sections of the application. */
|
|
1178
|
+
async function settingsCommand(input) {
|
|
1179
|
+
const action = input.shift();
|
|
1180
|
+
const project = await newProject();
|
|
1181
|
+
let result;
|
|
1182
|
+
if (action === 'list') {
|
|
1183
|
+
if (input.length)
|
|
1184
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1185
|
+
result = await project.settingsList(process.cwd());
|
|
1186
|
+
}
|
|
1187
|
+
else if (action === 'sync') {
|
|
1188
|
+
let dryRun = false;
|
|
1189
|
+
for (const arg of input) {
|
|
1190
|
+
if (arg === '--dry-run' && !dryRun)
|
|
1191
|
+
dryRun = true;
|
|
1192
|
+
else
|
|
1193
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1194
|
+
}
|
|
1195
|
+
mutationStarted = !dryRun;
|
|
1196
|
+
result = await project.settingsSync(process.cwd(), dryRun);
|
|
1197
|
+
}
|
|
1198
|
+
else if (action === 'add' || action === 'remove') {
|
|
1199
|
+
const section = input.shift();
|
|
1200
|
+
if (!section || section.startsWith('-'))
|
|
1201
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1202
|
+
let dryRun = false;
|
|
1203
|
+
for (const arg of input) {
|
|
1204
|
+
if (arg === '--dry-run' && !dryRun)
|
|
1205
|
+
dryRun = true;
|
|
1206
|
+
else
|
|
1207
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1208
|
+
}
|
|
1209
|
+
mutationStarted = !dryRun;
|
|
1210
|
+
result =
|
|
1211
|
+
action === 'add'
|
|
1212
|
+
? await project.settingsAdd(process.cwd(), section, { dryRun })
|
|
1213
|
+
: await project.settingsRemove(process.cwd(), section, dryRun);
|
|
1214
|
+
}
|
|
1215
|
+
else if (action && !action.startsWith('-'))
|
|
1216
|
+
throw unknownCommand(action, ['add', 'sync', 'remove', 'list'], 'settings ');
|
|
1217
|
+
else
|
|
1218
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
1219
|
+
process.stdout.write(JSON.stringify(machine ? envelope(`settings ${action}`, result) : result, null, 2) + '\n');
|
|
1220
|
+
}
|
|
741
1221
|
await main().catch((error) => {
|
|
742
1222
|
if (machine)
|
|
743
1223
|
process.stdout.write(JSON.stringify(failure(command, error, mutationStarted)) + '\n');
|
|
@@ -788,6 +1268,9 @@ function nextSteps(error) {
|
|
|
788
1268
|
const hint = error.details.hint;
|
|
789
1269
|
if (typeof hint === 'string')
|
|
790
1270
|
lines.add(`hint: ${hint}`);
|
|
1271
|
+
const fix = error.details.fix;
|
|
1272
|
+
if (typeof fix === 'string')
|
|
1273
|
+
lines.add(`fix: ${fix}`);
|
|
791
1274
|
// A decision that belongs to the user (PREFLIGHT_FAILED with a taken port, AI-M03).
|
|
792
1275
|
const { question, next, waitForUser } = error.details;
|
|
793
1276
|
if (typeof question === 'string')
|