@dokebi/cli 0.2.5 → 0.4.0
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 +181 -87
- package/README.md +55 -24
- package/catalog.json +2199 -0
- package/dist/app-urls.d.ts +21 -0
- package/dist/app-urls.js +94 -0
- package/dist/banner-art.d.ts +4 -0
- package/dist/banner-art.js +31 -0
- package/dist/banner.d.ts +25 -0
- package/dist/banner.js +202 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.js +475 -46
- package/dist/catalog.d.ts +161 -0
- package/dist/catalog.js +110 -0
- package/dist/check/form-rules.d.ts +13 -0
- package/dist/check/form-rules.js +224 -0
- package/dist/check/index.d.ts +23 -0
- package/dist/check/index.js +70 -0
- package/dist/check/packages.d.ts +17 -0
- package/dist/check/packages.js +161 -0
- package/dist/check/problems.d.ts +19 -0
- package/dist/check/problems.js +22 -0
- package/dist/check/project-rules.d.ts +10 -0
- package/dist/check/project-rules.js +142 -0
- package/dist/check/source-rules.d.ts +28 -0
- package/dist/check/source-rules.js +273 -0
- package/dist/check/typescript.d.ts +46 -0
- package/dist/check/typescript.js +168 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +372 -0
- package/dist/config-edit.d.ts +5 -4
- package/dist/config-edit.js +9 -8
- package/dist/configuration.d.ts +1 -0
- package/dist/configuration.js +2 -1
- package/dist/dev-infra.d.ts +62 -0
- package/dist/dev-infra.js +215 -0
- package/dist/env-file.d.ts +24 -0
- package/dist/env-file.js +100 -0
- package/dist/generator.d.ts +24 -1
- package/dist/generator.js +57 -47
- package/dist/guide.d.ts +29 -0
- package/dist/guide.js +90 -0
- package/dist/install-plan.d.ts +8 -0
- package/dist/install-plan.js +21 -4
- package/dist/layout.d.ts +5 -4
- package/dist/layout.js +8 -7
- package/dist/log-writer-control.d.ts +55 -0
- package/dist/log-writer-control.js +327 -0
- package/dist/log-writer.js +53 -12
- package/dist/manifest.d.ts +28 -0
- package/dist/manifest.js +77 -2
- package/dist/npm-client.d.ts +49 -0
- package/dist/npm-client.js +162 -41
- package/dist/output.d.ts +1 -1
- package/dist/output.js +14 -8
- package/dist/plugin-lifecycle.d.ts +7 -0
- package/dist/plugin-lifecycle.js +21 -3
- package/dist/plugins-command.d.ts +47 -0
- package/dist/plugins-command.js +189 -0
- package/dist/project-guide.d.ts +36 -1
- package/dist/project-guide.js +141 -18
- package/dist/project-inspector.d.ts +10 -0
- package/dist/project-inspector.js +51 -2
- package/dist/project-state.d.ts +13 -2
- package/dist/project-state.js +8 -6
- package/dist/project.d.ts +89 -7
- package/dist/project.js +835 -85
- package/dist/recipe-record.d.ts +24 -0
- package/dist/recipe-record.js +67 -0
- package/dist/recipe.d.ts +120 -0
- package/dist/recipe.js +793 -0
- package/dist/registry.d.ts +79 -0
- package/dist/registry.js +212 -0
- package/dist/rotating-log.d.ts +12 -0
- package/dist/rotating-log.js +19 -0
- package/dist/server.d.ts +94 -4
- package/dist/server.js +624 -64
- package/dist/verify.d.ts +62 -0
- package/dist/verify.js +380 -0
- package/dist/vite-host-process.d.ts +20 -0
- package/dist/vite-host-process.js +117 -0
- package/package.json +30 -6
package/dist/bin.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { writeBanner } from './banner.js';
|
|
3
|
+
import { loadCatalog } from './catalog.js';
|
|
4
|
+
import { check, checkText } from './check/index.js';
|
|
5
|
+
import { COMMAND_DETAILS, COMMANDS } from './commands.js';
|
|
2
6
|
import { parseSetOption } from './config-edit.js';
|
|
3
7
|
import { CliError } from './errors.js';
|
|
8
|
+
import { buildGuide, HELP_POINTER } from './guide.js';
|
|
4
9
|
import { CLI_VERSION, envelope, failure } from './output.js';
|
|
10
|
+
import { pluginsDoc, pluginsInfo, pluginsList, pluginsSearch } from './plugins-command.js';
|
|
5
11
|
import { Project } from './project.js';
|
|
6
12
|
import { ProjectInspector } from './project-inspector.js';
|
|
13
|
+
import { listRecipes, runRecipe } from './recipe.js';
|
|
7
14
|
import * as server from './server.js';
|
|
8
|
-
|
|
15
|
+
import { verify } from './verify.js';
|
|
16
|
+
/** The usage lines only (their format is stable; tools parse them). */
|
|
17
|
+
const USAGE_LINES = COMMANDS.map((item) => item.usage).join('\n') + '\n';
|
|
18
|
+
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`;
|
|
19
|
+
/** Names the top-level dispatcher knows (`app`, `module`, `plugins`, `recipe`, `config` take more). */
|
|
20
|
+
const TOP_LEVEL = [...new Set(COMMANDS.map((item) => item.name.split(' ')[0]))];
|
|
21
|
+
/** Levenshtein distance, small inputs only (command names). */
|
|
22
|
+
function distance(a, b) {
|
|
23
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
24
|
+
for (let i = 1; i <= a.length; i++) {
|
|
25
|
+
const current = [i];
|
|
26
|
+
for (let j = 1; j <= b.length; j++)
|
|
27
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
28
|
+
previous = current;
|
|
29
|
+
}
|
|
30
|
+
return previous[b.length];
|
|
31
|
+
}
|
|
32
|
+
/** Close names for an unknown command: same prefix or at most 2 edits (up to 3, best first). */
|
|
33
|
+
export function suggest(input, names) {
|
|
34
|
+
const aliases = {
|
|
35
|
+
help: '--help',
|
|
36
|
+
version: '--version',
|
|
37
|
+
'-v': '--version',
|
|
38
|
+
};
|
|
39
|
+
const alias = aliases[input];
|
|
40
|
+
if (alias)
|
|
41
|
+
return [alias];
|
|
42
|
+
return names
|
|
43
|
+
.map((name) => ({ name, score: distance(input, name) }))
|
|
44
|
+
.filter((item) => item.score <= Math.min(2, Math.floor(item.name.length / 2)) ||
|
|
45
|
+
(input.length >= 3 && item.name.startsWith(input)))
|
|
46
|
+
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
47
|
+
.slice(0, 3)
|
|
48
|
+
.map((item) => item.name);
|
|
49
|
+
}
|
|
50
|
+
function unknownCommand(input, names, prefix = '') {
|
|
51
|
+
const suggestions = suggest(input, names).map((name) => name.startsWith('-') ? name : `${prefix}${name}`);
|
|
52
|
+
const shown = `${prefix}${input}`;
|
|
53
|
+
return new CliError('UNKNOWN_COMMAND', suggestions.length
|
|
54
|
+
? `Unknown command "${shown}". Did you mean ${suggestions.map((name) => `"dokebi ${name}"`).join(' or ')}?`
|
|
55
|
+
: `Unknown command "${shown}". Run dokebi --help for the commands.`, { details: { command: shown, suggestions }, phase: 'arguments' });
|
|
56
|
+
}
|
|
57
|
+
/** `dokebi <command> [<subcommand>] --help`: that command's usage and summary. */
|
|
58
|
+
function commandHelp(words) {
|
|
59
|
+
const [first, second] = words;
|
|
60
|
+
if (!first || first.startsWith('-'))
|
|
61
|
+
return undefined;
|
|
62
|
+
const exact = second && !second.startsWith('-') ? `${first} ${second}` : undefined;
|
|
63
|
+
const matched = COMMANDS.filter((item) => exact && COMMANDS.some((other) => other.name === exact)
|
|
64
|
+
? item.name === exact
|
|
65
|
+
: item.name === first || item.name.startsWith(`${first} `));
|
|
66
|
+
if (!matched.length)
|
|
67
|
+
return undefined;
|
|
68
|
+
return `${matched
|
|
69
|
+
.map((item) => {
|
|
70
|
+
const details = COMMAND_DETAILS[item.name];
|
|
71
|
+
const width = Math.max(0, ...(details?.flags ?? []).map(([flag]) => flag.length));
|
|
72
|
+
return [
|
|
73
|
+
item.usage,
|
|
74
|
+
` ${item.summary}`,
|
|
75
|
+
...(details?.flags.length
|
|
76
|
+
? [
|
|
77
|
+
' flags:',
|
|
78
|
+
...details.flags.map(([flag, text]) => ` ${flag.padEnd(width)} ${text}`),
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
81
|
+
...(details ? [` example:\n${details.example.replace(/^/gm, ' ')}`] : []),
|
|
82
|
+
].join('\n');
|
|
83
|
+
})
|
|
84
|
+
.join('\n')}\n`;
|
|
85
|
+
}
|
|
9
86
|
const raw = process.argv.slice(2);
|
|
10
87
|
const machine = raw.includes('--json');
|
|
11
88
|
const args = raw.filter((arg) => arg !== '--json');
|
|
@@ -16,6 +93,7 @@ function flags(input, allowed) {
|
|
|
16
93
|
let dryRun = false;
|
|
17
94
|
let to;
|
|
18
95
|
const sets = [];
|
|
96
|
+
const add = {};
|
|
19
97
|
for (let index = 0; index < input.length; index++) {
|
|
20
98
|
const arg = input[index];
|
|
21
99
|
if (arg === '--dry-run' && !dryRun)
|
|
@@ -23,47 +101,177 @@ function flags(input, allowed) {
|
|
|
23
101
|
else if (arg === '--set' && allowed.set) {
|
|
24
102
|
const value = input[++index];
|
|
25
103
|
if (value === undefined || value.startsWith('--'))
|
|
26
|
-
throw new CliError('USAGE',
|
|
104
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
27
105
|
sets.push(parseSetOption(value));
|
|
28
106
|
}
|
|
29
107
|
else if (arg === '--to' && allowed.to && to === undefined) {
|
|
30
108
|
const value = input[++index];
|
|
31
109
|
if (!value || value.startsWith('-'))
|
|
32
|
-
throw new CliError('USAGE',
|
|
110
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
33
111
|
to = value;
|
|
34
112
|
}
|
|
113
|
+
else if (arg === '--adapter' && allowed.add && add.adapter === undefined) {
|
|
114
|
+
const value = input[++index];
|
|
115
|
+
if (!value || value.startsWith('-'))
|
|
116
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
117
|
+
add.adapter = value;
|
|
118
|
+
}
|
|
119
|
+
else if (arg === '--no-first-app' && allowed.add && add.firstApp === undefined)
|
|
120
|
+
add.firstApp = false;
|
|
35
121
|
else
|
|
36
|
-
throw new CliError('USAGE',
|
|
122
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
37
123
|
}
|
|
38
|
-
return { dryRun, sets, to };
|
|
124
|
+
return { dryRun, sets, to, add };
|
|
125
|
+
}
|
|
126
|
+
/** Prints a result: the envelope with --json, else pretty JSON (or `text` when given). */
|
|
127
|
+
function print(name, result, text, ok = true) {
|
|
128
|
+
process.stdout.write(machine
|
|
129
|
+
? JSON.stringify(envelope(name, result, ok)) + '\n'
|
|
130
|
+
: text !== undefined
|
|
131
|
+
? text + (text.endsWith('\n') ? '' : '\n')
|
|
132
|
+
: JSON.stringify(result, null, 2) + '\n');
|
|
39
133
|
}
|
|
40
134
|
async function main() {
|
|
41
135
|
if (raw.filter((arg) => arg === '--json').length > 1)
|
|
42
|
-
throw new CliError('USAGE',
|
|
43
|
-
if (command
|
|
136
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
137
|
+
if (command !== '--help' &&
|
|
138
|
+
command !== '-h' &&
|
|
139
|
+
(args.includes('--help') || args.includes('-h'))) {
|
|
140
|
+
// `dokebi <command> [<subcommand>] --help`: that command only, exit 0.
|
|
141
|
+
const words = [command, ...args.filter((arg) => arg !== '--help' && arg !== '-h')];
|
|
142
|
+
const text = commandHelp(words);
|
|
143
|
+
if (!text)
|
|
144
|
+
throw unknownCommand(command, [...TOP_LEVEL, '--help', '--version']);
|
|
145
|
+
process.stdout.write(machine ? JSON.stringify(envelope(command, text)) + '\n' : text);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (command === '--help' || command === '-h' || command === '--version') {
|
|
44
149
|
if (args.length)
|
|
45
|
-
throw new CliError('USAGE',
|
|
46
|
-
const value = command === '--
|
|
150
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
151
|
+
const value = command === '--version' ? CLI_VERSION : HELP;
|
|
152
|
+
if (command !== '--version')
|
|
153
|
+
writeBanner(process.stderr, machine);
|
|
47
154
|
process.stdout.write(machine
|
|
48
155
|
? JSON.stringify(envelope(command, value)) + '\n'
|
|
49
156
|
: value + (value.endsWith('\n') ? '' : '\n'));
|
|
50
157
|
return;
|
|
51
158
|
}
|
|
52
|
-
if (command === '
|
|
159
|
+
if (command === 'guide') {
|
|
160
|
+
if (args.length)
|
|
161
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
162
|
+
const guide = buildGuide(await loadCatalog());
|
|
163
|
+
writeBanner(process.stderr, machine);
|
|
164
|
+
print(command, guide, guide.text);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (command === 'plugins') {
|
|
168
|
+
await pluginsCommand(args);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (command === 'recipe') {
|
|
172
|
+
await recipeCommand(args);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (command === 'verify') {
|
|
176
|
+
const options = serverFlags(args, { '--wait-ms': undefined }, []);
|
|
177
|
+
const waitMs = options.numbers.get('--wait-ms');
|
|
178
|
+
const result = await verify(process.cwd(), waitMs !== undefined ? { waitMs } : {});
|
|
179
|
+
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.tellUser
|
|
182
|
+
? `\nTell the user (your final message must include these lines):\n${result.tellUser.map((line) => `- ${line}`).join('\n')}`
|
|
183
|
+
: ''}`, result.ok);
|
|
184
|
+
if (!result.ok)
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (command === 'check') {
|
|
53
189
|
if (args.length)
|
|
54
|
-
throw new CliError('USAGE',
|
|
190
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
191
|
+
const result = await check(process.cwd());
|
|
192
|
+
print(command, result, checkText(result), result.ok);
|
|
193
|
+
if (!result.ok)
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (command === 'config') {
|
|
198
|
+
const action = args.shift();
|
|
199
|
+
if (action === 'secret') {
|
|
200
|
+
const dryRun = args.includes('--dry-run');
|
|
201
|
+
const rest = args.filter((arg) => arg !== '--dry-run');
|
|
202
|
+
const target = rest[0];
|
|
203
|
+
const dot = target?.indexOf('.') ?? -1;
|
|
204
|
+
if (rest.length !== 1 || !target || dot <= 0 || target.includes('='))
|
|
205
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
206
|
+
// The path is checked like a --set path (segments, no prototype keys).
|
|
207
|
+
const path = parseSetOption(`${target.slice(dot + 1)}=0`).path;
|
|
208
|
+
mutationStarted = true;
|
|
209
|
+
const result = await new Project().configSecret(process.cwd(), target.slice(0, dot), path, dryRun);
|
|
210
|
+
print('config secret', result);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (action !== 'set') {
|
|
214
|
+
if (action && !action.startsWith('-'))
|
|
215
|
+
throw unknownCommand(action, ['set', 'secret'], 'config ');
|
|
216
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
217
|
+
}
|
|
218
|
+
let dryRun = false;
|
|
219
|
+
const assignments = [];
|
|
220
|
+
for (const arg of args) {
|
|
221
|
+
if (arg === '--dry-run' && !dryRun)
|
|
222
|
+
dryRun = true;
|
|
223
|
+
else if (!arg.startsWith('-')) {
|
|
224
|
+
// <plugin-id>.<path>=<value>: the plugin id has no dot; the path is dot separated.
|
|
225
|
+
const dot = arg.indexOf('.');
|
|
226
|
+
const equals = arg.indexOf('=');
|
|
227
|
+
if (dot <= 0 || equals < dot)
|
|
228
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
229
|
+
assignments.push({ plugin: arg.slice(0, dot), set: parseSetOption(arg.slice(dot + 1)) });
|
|
230
|
+
}
|
|
231
|
+
else
|
|
232
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
233
|
+
}
|
|
234
|
+
if (!assignments.length)
|
|
235
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
236
|
+
mutationStarted = true;
|
|
237
|
+
const result = await new Project().configSet(process.cwd(), assignments, dryRun);
|
|
238
|
+
printWarnings(result.warnings);
|
|
239
|
+
print('config set', result);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (command === 'dev-infra') {
|
|
243
|
+
const dryRun = args.includes('--dry-run');
|
|
244
|
+
const options = serverFlags(args.filter((arg) => arg !== '--dry-run'), { '--db-port': [1, 65535] }, []);
|
|
245
|
+
mutationStarted = true;
|
|
246
|
+
const dbPort = options.numbers.get('--db-port');
|
|
247
|
+
const result = await new Project().devInfra(process.cwd(), {
|
|
248
|
+
dryRun,
|
|
249
|
+
...(dbPort !== undefined ? { dbPort } : {}),
|
|
250
|
+
});
|
|
251
|
+
print(command, result);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (command === 'inspect' || command === 'doctor') {
|
|
255
|
+
const verbose = args.length === 1 && args[0] === '--verbose';
|
|
256
|
+
if (args.length && !verbose)
|
|
257
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
55
258
|
const result = await new ProjectInspector().inspect(process.cwd(), {
|
|
56
259
|
runChecks: command === 'doctor',
|
|
57
260
|
});
|
|
58
261
|
const ok = command === 'inspect' || result.healthy;
|
|
59
|
-
|
|
262
|
+
// --json is always complete (summary first); people get the short form unless --verbose.
|
|
263
|
+
process.stdout.write(machine
|
|
264
|
+
? `${JSON.stringify(envelope(command, result, ok), null, 2)}\n`
|
|
265
|
+
: verbose
|
|
266
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
267
|
+
: inspectionText(result));
|
|
60
268
|
if (!ok)
|
|
61
269
|
process.exitCode = 1;
|
|
62
270
|
return;
|
|
63
271
|
}
|
|
64
272
|
if (command === 'migrate') {
|
|
65
273
|
if (args.length && (args.length !== 1 || args[0] !== '--dry-run'))
|
|
66
|
-
throw new CliError('USAGE',
|
|
274
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
67
275
|
mutationStarted = true;
|
|
68
276
|
const result = await new Project().migrate(process.cwd(), args[0] === '--dry-run');
|
|
69
277
|
process.stdout.write(JSON.stringify(machine ? envelope(command, result) : result, null, 2) + '\n');
|
|
@@ -81,22 +289,36 @@ async function main() {
|
|
|
81
289
|
await moduleCommand(args);
|
|
82
290
|
return;
|
|
83
291
|
}
|
|
292
|
+
if (!['init', 'add', 'upgrade', 'remove'].includes(command))
|
|
293
|
+
throw unknownCommand(command, [...TOP_LEVEL, '--help', '--version']);
|
|
84
294
|
const target = args.shift();
|
|
85
295
|
if (!target || target.startsWith('-'))
|
|
86
|
-
throw new CliError('USAGE',
|
|
296
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
87
297
|
const project = new Project();
|
|
88
298
|
let result;
|
|
89
299
|
if (command === 'init') {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
300
|
+
let core;
|
|
301
|
+
let dryRun = false;
|
|
302
|
+
for (let index = 0; index < args.length; index++) {
|
|
303
|
+
const arg = args[index];
|
|
304
|
+
if (arg === '--core' && core === undefined) {
|
|
305
|
+
core = args[++index];
|
|
306
|
+
if (!core || core.startsWith('-'))
|
|
307
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
308
|
+
}
|
|
309
|
+
else if (arg === '--dry-run' && !dryRun)
|
|
310
|
+
dryRun = true;
|
|
311
|
+
else
|
|
312
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
313
|
+
}
|
|
314
|
+
writeBanner(process.stderr, machine);
|
|
315
|
+
mutationStarted = !dryRun;
|
|
316
|
+
result = await project.init(target, core, dryRun);
|
|
95
317
|
}
|
|
96
318
|
else if (command === 'add') {
|
|
97
|
-
const options = flags(args, { set: true });
|
|
319
|
+
const options = flags(args, { set: true, add: true });
|
|
98
320
|
mutationStarted = true;
|
|
99
|
-
result = await project.add(process.cwd(), target, options.dryRun, options.sets);
|
|
321
|
+
result = await project.add(process.cwd(), target, options.dryRun, options.sets, options.add);
|
|
100
322
|
}
|
|
101
323
|
else if (command === 'upgrade') {
|
|
102
324
|
const options = flags(args, { to: true });
|
|
@@ -109,12 +331,187 @@ async function main() {
|
|
|
109
331
|
result = await project.remove(process.cwd(), target, options.dryRun);
|
|
110
332
|
}
|
|
111
333
|
else
|
|
112
|
-
throw new CliError('USAGE',
|
|
113
|
-
|
|
114
|
-
for (const warning of result.warnings ?? [])
|
|
115
|
-
if (warning.code === 'SECRET_ON_COMMAND_LINE')
|
|
116
|
-
process.stderr.write(`warning: ${warning.path} looks secret; command-line values can remain in shell history\n`);
|
|
334
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
335
|
+
printWarnings(result.warnings);
|
|
117
336
|
process.stdout.write(JSON.stringify(machine ? envelope(command, result) : result, null, 2) + '\n');
|
|
337
|
+
// stdout stays the result document; the same next steps are printed for people on stderr.
|
|
338
|
+
if (!machine && result.nextSteps?.length)
|
|
339
|
+
process.stderr.write(`next steps:\n${result.nextSteps.map((step) => ` ${step.command}\n ${step.why}\n`).join('')}`);
|
|
340
|
+
}
|
|
341
|
+
/** The short human form of inspect/doctor: summary, each diagnostic and plugin on one line. */
|
|
342
|
+
function inspectionText(result) {
|
|
343
|
+
const { summary } = result;
|
|
344
|
+
const lines = [
|
|
345
|
+
`${summary.healthy ? 'healthy' : 'NOT healthy'}: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.plugins} plugins, core ${result.coreVersion ?? '-'}`,
|
|
346
|
+
...result.diagnostics.map((item) => `${item.severity === 'error' ? 'error ' : 'warning'} ${item.code} ${item.path}${item.plugin ? ` (${item.plugin})` : ''}${item.hint ? `\n hint: ${item.hint}` : ''}`),
|
|
347
|
+
...result.plugins.map((item) => `plugin ${item.expected.id} ${item.installed?.version ?? 'MISSING'}${item.documentation ? ` · ${item.documentation}` : ''}`),
|
|
348
|
+
'details: --verbose (or --json for the full document)',
|
|
349
|
+
];
|
|
350
|
+
return `${lines.join('\n')}\n`;
|
|
351
|
+
}
|
|
352
|
+
/** Human output: warnings people should see go to stderr (the result stays on stdout). */
|
|
353
|
+
function printWarnings(warnings) {
|
|
354
|
+
if (machine)
|
|
355
|
+
return;
|
|
356
|
+
for (const warning of warnings ?? [])
|
|
357
|
+
if (warning.code === 'SECRET_ON_COMMAND_LINE')
|
|
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')
|
|
360
|
+
process.stderr.write(`warning: ${warning.path} is not a @dokebi package; install it only with the user's consent\n`);
|
|
361
|
+
}
|
|
362
|
+
/** `dokebi plugins list|search|info|doc` (D-0065). */
|
|
363
|
+
async function pluginsCommand(input) {
|
|
364
|
+
const action = input.shift();
|
|
365
|
+
const name = `plugins ${action}`;
|
|
366
|
+
if (action === 'list') {
|
|
367
|
+
let compatible = false;
|
|
368
|
+
let online = false;
|
|
369
|
+
for (const arg of input) {
|
|
370
|
+
if (arg === '--compatible' && !compatible)
|
|
371
|
+
compatible = true;
|
|
372
|
+
else if (arg === '--online' && !online)
|
|
373
|
+
online = true;
|
|
374
|
+
else
|
|
375
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
376
|
+
}
|
|
377
|
+
const result = await pluginsList(process.cwd(), { compatible, online });
|
|
378
|
+
print(name, result, result.packages
|
|
379
|
+
.map((item) => `${item.package}@${item.version}${item.installed ? ` (installed ${item.installed})` : ''}${item.compatible ? '' : ' [not for this core]'}\n ${item.summary}\n provides: ${item.provides.join(', ') || '-'} · ${item.install}`)
|
|
380
|
+
.join('\n') +
|
|
381
|
+
`\n${result.warnings.map((warning) => `warning: ${warning.code} ${warning.message}`).join('\n')}`);
|
|
382
|
+
}
|
|
383
|
+
else if (action === 'search') {
|
|
384
|
+
const words = input.filter((arg) => !arg.startsWith('-'));
|
|
385
|
+
if (!words.length || words.length !== input.length)
|
|
386
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
387
|
+
const result = await pluginsSearch(words.join(' '));
|
|
388
|
+
print(name, result, [
|
|
389
|
+
...result.matches.map((item) => `${item.package}@${item.version}: ${item.summary}`),
|
|
390
|
+
...result.recipes.map((recipe) => `recipe ${recipe.name}: ${recipe.summary}`),
|
|
391
|
+
...result.absent.map((entry) => `ABSENT ${entry.capability}: ${entry.summary}. ${entry.instead}`),
|
|
392
|
+
...(result.status === 'unknown' ? ['nothing found (not in the catalog)'] : []),
|
|
393
|
+
].join('\n'));
|
|
394
|
+
}
|
|
395
|
+
else if (action === 'info' || action === 'doc') {
|
|
396
|
+
const key = input.shift();
|
|
397
|
+
if (!key || key.startsWith('-') || input.length)
|
|
398
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
399
|
+
if (action === 'info')
|
|
400
|
+
print(name, await pluginsInfo(process.cwd(), key));
|
|
401
|
+
else {
|
|
402
|
+
const result = await pluginsDoc(process.cwd(), key);
|
|
403
|
+
print(name, result, `# ${result.path} (${result.package} ${result.version})\n\n${result.content}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (action && !action.startsWith('-'))
|
|
407
|
+
throw unknownCommand(action, ['list', 'search', 'info', 'doc'], 'plugins ');
|
|
408
|
+
else
|
|
409
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
410
|
+
}
|
|
411
|
+
/** `dokebi recipe list|<name>` (D-0065). Missing answers: questions, exit 2, nothing installed. */
|
|
412
|
+
async function recipeCommand(input) {
|
|
413
|
+
const name = input.shift();
|
|
414
|
+
if (!name || name.startsWith('-'))
|
|
415
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
416
|
+
if (name === 'list') {
|
|
417
|
+
if (input.length)
|
|
418
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
419
|
+
const result = await listRecipes();
|
|
420
|
+
print('recipe list', result, result.recipes
|
|
421
|
+
.map((recipe) => `${recipe.name}: ${recipe.summary}\n questions: ${recipe.questions.join(', ')}`)
|
|
422
|
+
.join('\n'));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const answers = {};
|
|
426
|
+
const flagOf = {
|
|
427
|
+
'--view': 'view',
|
|
428
|
+
'--db': 'db',
|
|
429
|
+
'--dev-infra': 'devInfra',
|
|
430
|
+
'--admin': 'admin',
|
|
431
|
+
};
|
|
432
|
+
let dir;
|
|
433
|
+
let defaults = false;
|
|
434
|
+
let answersFrom;
|
|
435
|
+
let dryRun = false;
|
|
436
|
+
let port;
|
|
437
|
+
let dbPort;
|
|
438
|
+
const number = (flag, value) => {
|
|
439
|
+
const parsed = Number(value);
|
|
440
|
+
if (value === undefined || !Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535)
|
|
441
|
+
throw new CliError('USAGE', `${flag} must be a port from 1 to 65535\n${USAGE_LINES}`);
|
|
442
|
+
return parsed;
|
|
443
|
+
};
|
|
444
|
+
for (let index = 0; index < input.length; index++) {
|
|
445
|
+
const arg = input[index];
|
|
446
|
+
const question = flagOf[arg];
|
|
447
|
+
if (question && answers[question] === undefined) {
|
|
448
|
+
const value = input[++index];
|
|
449
|
+
if (!value || value.startsWith('-'))
|
|
450
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
451
|
+
answers[question] = value;
|
|
452
|
+
}
|
|
453
|
+
else if (arg === '--dir' && dir === undefined) {
|
|
454
|
+
dir = input[++index];
|
|
455
|
+
if (!dir || dir.startsWith('-'))
|
|
456
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
457
|
+
}
|
|
458
|
+
else if (arg === '--port' && port === undefined)
|
|
459
|
+
port = number(arg, input[++index]);
|
|
460
|
+
else if (arg === '--db-port' && dbPort === undefined)
|
|
461
|
+
dbPort = number(arg, input[++index]);
|
|
462
|
+
else if (arg === '--defaults' && !defaults)
|
|
463
|
+
defaults = true;
|
|
464
|
+
else if (arg === '--answers-from' && answersFrom === undefined) {
|
|
465
|
+
const value = input[++index];
|
|
466
|
+
if (value !== 'user' && value !== 'defaults')
|
|
467
|
+
throw new CliError('USAGE', `--answers-from must be user (the user answered) or defaults (the user said to decide)\n${USAGE_LINES}`);
|
|
468
|
+
answersFrom = value;
|
|
469
|
+
}
|
|
470
|
+
else if (arg === '--dry-run' && !dryRun)
|
|
471
|
+
dryRun = true;
|
|
472
|
+
else
|
|
473
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
474
|
+
}
|
|
475
|
+
mutationStarted = !dryRun;
|
|
476
|
+
const result = await runRecipe({
|
|
477
|
+
name,
|
|
478
|
+
answers,
|
|
479
|
+
defaults,
|
|
480
|
+
...(answersFrom !== undefined ? { answersFrom } : {}),
|
|
481
|
+
dryRun,
|
|
482
|
+
...(dir !== undefined ? { dir } : {}),
|
|
483
|
+
...(port !== undefined ? { port } : {}),
|
|
484
|
+
...(dbPort !== undefined ? { dbPort } : {}),
|
|
485
|
+
}, new Project());
|
|
486
|
+
if (result.status === 'needs-answers') {
|
|
487
|
+
// Not an error of the command: the answers come from the user. Nothing was installed.
|
|
488
|
+
process.exitCode = 2;
|
|
489
|
+
if (machine)
|
|
490
|
+
process.stdout.write(JSON.stringify({
|
|
491
|
+
schemaVersion: 1,
|
|
492
|
+
cliVersion: CLI_VERSION,
|
|
493
|
+
command: `recipe ${name}`,
|
|
494
|
+
ok: false,
|
|
495
|
+
result,
|
|
496
|
+
error: {
|
|
497
|
+
code: 'ANSWERS_REQUIRED',
|
|
498
|
+
message: `Ask the user: ${result.missing.join(', ')} — then end your turn and wait for the answers`,
|
|
499
|
+
phase: 'arguments',
|
|
500
|
+
mutation: 'none',
|
|
501
|
+
details: { missing: result.missing },
|
|
502
|
+
},
|
|
503
|
+
}) + '\n');
|
|
504
|
+
else
|
|
505
|
+
process.stdout.write(`ANSWERS_REQUIRED — ask the user, then end your turn; install nothing before they answer (recipe ${name}: ${result.summary})\n\n${result.questions
|
|
506
|
+
.map((question) => `${question.flag} — ${question.question}${question.answer ? ` [answered: ${question.answer.value}]` : ''}${question.when ? ` (only when ${JSON.stringify(question.when)})` : ''}\n${question.options
|
|
507
|
+
.map((option) => ` ${option.value}${option.value === question.default ? ' (default)' : ''}${option.available ? '' : ' — NOT AVAILABLE'}: ${option.label}${option.why ? ` — ${option.why}` : ''}${option.note ? ` — NOTE: ${option.note}` : ''}${option.pros.length ? `\n + ${option.pros.join('\n + ')}` : ''}${option.cons.length ? `\n - ${option.cons.join('\n - ')}` : ''}`)
|
|
508
|
+
.join('\n')}\n default ${question.default}: ${question.defaultReason}`)
|
|
509
|
+
.join('\n\n')}\n\n${result.next}\n`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
print(`recipe ${name}`, result);
|
|
513
|
+
if (!machine && result.report)
|
|
514
|
+
process.stderr.write(`${result.report}\n`);
|
|
118
515
|
}
|
|
119
516
|
/**
|
|
120
517
|
* Reads `--name <number>` style flags of the server commands; anything else is a usage error.
|
|
@@ -131,11 +528,11 @@ function serverFlags(input, numbers, switches) {
|
|
|
131
528
|
const text = input[++index];
|
|
132
529
|
const value = Number(text);
|
|
133
530
|
if (text === undefined || !Number.isSafeInteger(value) || value < min || value > max)
|
|
134
|
-
throw new CliError('USAGE', `${arg} must be an integer from ${min} to ${max}\n${
|
|
531
|
+
throw new CliError('USAGE', `${arg} must be an integer from ${min} to ${max}\n${USAGE_LINES}`);
|
|
135
532
|
result.numbers.set(arg, value);
|
|
136
533
|
}
|
|
137
534
|
else
|
|
138
|
-
throw new CliError('USAGE',
|
|
535
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
139
536
|
}
|
|
140
537
|
return result;
|
|
141
538
|
}
|
|
@@ -145,7 +542,7 @@ async function serverCommand(input) {
|
|
|
145
542
|
const print = (result, text) => process.stdout.write(machine ? JSON.stringify(envelope(command, result)) + '\n' : text + '\n');
|
|
146
543
|
if (command === 'dev') {
|
|
147
544
|
if (input.length || machine)
|
|
148
|
-
throw new CliError('USAGE',
|
|
545
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
149
546
|
process.exitCode = await server.dev(root, (line) => process.stderr.write(`[dokebi dev] ${line}\n`));
|
|
150
547
|
}
|
|
151
548
|
else if (command === 'start') {
|
|
@@ -158,7 +555,7 @@ async function serverCommand(input) {
|
|
|
158
555
|
if (options.switches.has('--foreground')) {
|
|
159
556
|
// The foreground app writes to the terminal; rotation belongs to systemd/Docker there.
|
|
160
557
|
if (machine || options.numbers.size)
|
|
161
|
-
throw new CliError('USAGE',
|
|
558
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
162
559
|
process.exitCode = await server.foreground(root);
|
|
163
560
|
return;
|
|
164
561
|
}
|
|
@@ -173,7 +570,7 @@ async function serverCommand(input) {
|
|
|
173
570
|
if (!machine)
|
|
174
571
|
for (const warning of result.warnings)
|
|
175
572
|
process.stderr.write(`warning: ${warning}\n`);
|
|
176
|
-
print(result, `started pid ${result.pid}${result.ready ? ` (ready in ${result.ms} ms)` : ''}\nlog: ${result.log} (rotated at ${Math.round(result.logMaxBytes / 1048576)} MB, ${result.logFiles} older files kept)`);
|
|
573
|
+
print(result, `started pid ${result.pid}${result.ready ? ` (ready in ${result.ms} ms)` : ''}${addressLines(result)}\nlog: ${result.log} (rotated at ${Math.round(result.logMaxBytes / 1048576)} MB, ${result.logFiles} older files kept)`);
|
|
177
574
|
}
|
|
178
575
|
else if (command === 'stop') {
|
|
179
576
|
const options = serverFlags(input, { '--timeout-ms': undefined }, ['--force']);
|
|
@@ -182,14 +579,14 @@ async function serverCommand(input) {
|
|
|
182
579
|
for (const warning of result.warnings ?? [])
|
|
183
580
|
process.stderr.write(`warning: ${warning}\n`);
|
|
184
581
|
print(result, result.stopped
|
|
185
|
-
? `stopped pid ${result.pid} in ${result.ms} ms${result.forced ? ' (killed)' : ''}`
|
|
582
|
+
? `stopped pid ${result.pid} in ${result.ms} ms${result.forced ? ' (killed)' : ''}${result.proof === 'http' ? ' (no ps: the app proved its instance token over HTTP)' : result.proof === 'log-writer' ? ' (no ps: its log writer proved the instance token)' : ''}${result.via === 'log-writer' ? ' (signalled by its log writer: this shell may not signal it)' : ''}`
|
|
186
583
|
: result.stale
|
|
187
584
|
? `not running (pid ${result.pid} had already exited; removed the pid file)`
|
|
188
585
|
: 'not running');
|
|
189
586
|
}
|
|
190
587
|
else if (command === 'status') {
|
|
191
588
|
if (input.length)
|
|
192
|
-
throw new CliError('USAGE',
|
|
589
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
193
590
|
const result = await server.status(root);
|
|
194
591
|
const files = result.logFiles
|
|
195
592
|
.map((file) => `\n ${file.file} (${(file.bytes / 1048576).toFixed(1)} MB)`)
|
|
@@ -201,7 +598,7 @@ async function serverCommand(input) {
|
|
|
201
598
|
? `\nlog writer diagnostics: ${result.logWriterLog}`
|
|
202
599
|
: '';
|
|
203
600
|
print(result, (result.running
|
|
204
|
-
? `running pid ${result.pid} · up ${result.uptimeSec} s · ${result.rssMb} MB${writer}`
|
|
601
|
+
? `running pid ${result.pid} · up ${result.uptimeSec} s${result.rssMb !== undefined ? ` · ${result.rssMb} MB` : ''}${result.proof === 'http' ? ' · proof: http (no ps; the app answered its instance token)' : result.proof === 'log-writer' ? ' · proof: log-writer (no ps; its log writer proved the instance token)' : ''}${writer}`
|
|
205
602
|
: result.stale
|
|
206
603
|
? `stopped: pid ${result.pid} exited without dokebi stop (crashed or killed); see dokebi logs`
|
|
207
604
|
: 'stopped') +
|
|
@@ -212,7 +609,7 @@ async function serverCommand(input) {
|
|
|
212
609
|
}
|
|
213
610
|
else {
|
|
214
611
|
if (machine)
|
|
215
|
-
throw new CliError('USAGE',
|
|
612
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
216
613
|
const options = serverFlags(input, { '-n': undefined }, ['-f', '--follow']);
|
|
217
614
|
await server.logs(root, options.numbers.get('-n'), options.switches.has('-f') || options.switches.has('--follow'));
|
|
218
615
|
}
|
|
@@ -224,13 +621,13 @@ async function app(input) {
|
|
|
224
621
|
let result;
|
|
225
622
|
if (action === 'list') {
|
|
226
623
|
if (input.length)
|
|
227
|
-
throw new CliError('USAGE',
|
|
624
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
228
625
|
result = await project.appList(process.cwd());
|
|
229
626
|
}
|
|
230
627
|
else if (action === 'add' || action === 'remove') {
|
|
231
628
|
const name = input.shift();
|
|
232
629
|
if (!name || name.startsWith('-'))
|
|
233
|
-
throw new CliError('USAGE',
|
|
630
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
234
631
|
let dryRun = false;
|
|
235
632
|
let inertia = false;
|
|
236
633
|
let ssr = false;
|
|
@@ -254,11 +651,11 @@ async function app(input) {
|
|
|
254
651
|
index++;
|
|
255
652
|
}
|
|
256
653
|
else
|
|
257
|
-
throw new CliError('USAGE',
|
|
654
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
258
655
|
}
|
|
259
656
|
// Server rendering (D-0039) is a choice of an Inertia app.
|
|
260
657
|
if (ssr && !inertia)
|
|
261
|
-
throw new CliError('USAGE',
|
|
658
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
262
659
|
mutationStarted = true;
|
|
263
660
|
result =
|
|
264
661
|
action === 'add'
|
|
@@ -275,7 +672,7 @@ async function app(input) {
|
|
|
275
672
|
const first = input.shift();
|
|
276
673
|
const domain = action === 'link' ? input.shift() : first;
|
|
277
674
|
if (!first || first.startsWith('-') || !domain || domain.startsWith('-'))
|
|
278
|
-
throw new CliError('USAGE',
|
|
675
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
279
676
|
let dryRun = false;
|
|
280
677
|
let template;
|
|
281
678
|
for (let index = 0; index < input.length; index++) {
|
|
@@ -288,7 +685,7 @@ async function app(input) {
|
|
|
288
685
|
index++;
|
|
289
686
|
}
|
|
290
687
|
else
|
|
291
|
-
throw new CliError('USAGE',
|
|
688
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
292
689
|
}
|
|
293
690
|
mutationStarted = true;
|
|
294
691
|
result =
|
|
@@ -299,8 +696,10 @@ async function app(input) {
|
|
|
299
696
|
})
|
|
300
697
|
: await project.appUnlink(process.cwd(), domain, dryRun);
|
|
301
698
|
}
|
|
699
|
+
else if (action && !action.startsWith('-'))
|
|
700
|
+
throw unknownCommand(action, ['add', 'remove', 'link', 'unlink', 'list'], 'app ');
|
|
302
701
|
else
|
|
303
|
-
throw new CliError('USAGE',
|
|
702
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
304
703
|
process.stdout.write(JSON.stringify(machine ? envelope(`app ${action}`, result) : result, null, 2) + '\n');
|
|
305
704
|
}
|
|
306
705
|
/** `dokebi module add|remove|list` (D-0032): feature modules in src/shared/modules/<name>. */
|
|
@@ -310,13 +709,13 @@ async function moduleCommand(input) {
|
|
|
310
709
|
let result;
|
|
311
710
|
if (action === 'list') {
|
|
312
711
|
if (input.length)
|
|
313
|
-
throw new CliError('USAGE',
|
|
712
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
314
713
|
result = await project.moduleList(process.cwd());
|
|
315
714
|
}
|
|
316
715
|
else if (action === 'add' || action === 'remove') {
|
|
317
716
|
const name = input.shift();
|
|
318
717
|
if (!name || name.startsWith('-'))
|
|
319
|
-
throw new CliError('USAGE',
|
|
718
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
320
719
|
let dryRun = false;
|
|
321
720
|
let views = true;
|
|
322
721
|
for (const arg of input) {
|
|
@@ -325,7 +724,7 @@ async function moduleCommand(input) {
|
|
|
325
724
|
else if (action === 'add' && arg === '--no-views' && views)
|
|
326
725
|
views = false;
|
|
327
726
|
else
|
|
328
|
-
throw new CliError('USAGE',
|
|
727
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
329
728
|
}
|
|
330
729
|
mutationStarted = true;
|
|
331
730
|
result =
|
|
@@ -333,8 +732,10 @@ async function moduleCommand(input) {
|
|
|
333
732
|
? await project.moduleAdd(process.cwd(), name, { views, dryRun })
|
|
334
733
|
: await project.moduleRemove(process.cwd(), name, dryRun);
|
|
335
734
|
}
|
|
735
|
+
else if (action && !action.startsWith('-'))
|
|
736
|
+
throw unknownCommand(action, ['add', 'remove', 'list'], 'module ');
|
|
336
737
|
else
|
|
337
|
-
throw new CliError('USAGE',
|
|
738
|
+
throw new CliError('USAGE', USAGE_LINES);
|
|
338
739
|
process.stdout.write(JSON.stringify(machine ? envelope(`module ${action}`, result) : result, null, 2) + '\n');
|
|
339
740
|
}
|
|
340
741
|
await main().catch((error) => {
|
|
@@ -346,6 +747,17 @@ await main().catch((error) => {
|
|
|
346
747
|
nextSteps(error));
|
|
347
748
|
process.exitCode = 1;
|
|
348
749
|
});
|
|
750
|
+
/**
|
|
751
|
+
* `urls:` (where the app answers: its loopback domains) and, when it differs, `listen:` (the
|
|
752
|
+
* bound address, which answers only the Host names of the app domains) — AI-M03.
|
|
753
|
+
*/
|
|
754
|
+
function addressLines(result) {
|
|
755
|
+
const same = result.urls.length === result.listen.length &&
|
|
756
|
+
result.urls.every((url, index) => url === result.listen[index]);
|
|
757
|
+
return `${result.urls.length ? `\nurls: ${result.urls.join(' ')}` : ''}${result.listen.length && !same
|
|
758
|
+
? `\nlisten: ${result.listen.join(' ')} (the bound address${result.urls.length ? '' : '; it answers only the Host names of the app domains, none of which is a loopback name such as localhost'})`
|
|
759
|
+
: ''}`;
|
|
760
|
+
}
|
|
349
761
|
/** Human-readable follow-ups from error details: missing option fields and plugin hints. */
|
|
350
762
|
function nextSteps(error) {
|
|
351
763
|
if (!(error instanceof CliError) || !error.details || typeof error.details !== 'object')
|
|
@@ -359,6 +771,23 @@ function nextSteps(error) {
|
|
|
359
771
|
const tail = error.details.tail;
|
|
360
772
|
if (typeof tail === 'string' && tail)
|
|
361
773
|
lines.add(`last log lines:\n${tail.trimEnd()}`);
|
|
774
|
+
const npmError = error.details.stderr;
|
|
775
|
+
if (error.code === 'NPM_FAILED' && typeof npmError === 'string' && npmError.trim())
|
|
776
|
+
lines.add(`npm stderr (end):\n${npmError.trimEnd()}`);
|
|
777
|
+
if (error.code === 'UNKNOWN_COMMAND')
|
|
778
|
+
lines.add('hint: dokebi --help lists the commands');
|
|
779
|
+
const hint = error.details.hint;
|
|
780
|
+
if (typeof hint === 'string')
|
|
781
|
+
lines.add(`hint: ${hint}`);
|
|
782
|
+
// A decision that belongs to the user (PREFLIGHT_FAILED with a taken port, AI-M03).
|
|
783
|
+
const { question, next, waitForUser } = error.details;
|
|
784
|
+
if (typeof question === 'string')
|
|
785
|
+
lines.add(`${waitForUser === true ? 'ask the user' : 'question'}: ${question}`);
|
|
786
|
+
if (typeof next === 'string')
|
|
787
|
+
lines.add(`next: ${next}`);
|
|
788
|
+
const manual = error.details.manualStop;
|
|
789
|
+
if (typeof manual === 'string')
|
|
790
|
+
lines.add(`manual stop: ${manual}`);
|
|
362
791
|
if (error.code === 'WORKSPACE_SYNC_FAILED')
|
|
363
792
|
lines.add('hint: run npm install at the workspace root');
|
|
364
793
|
if (Array.isArray(details.diagnostics))
|