@wpmoo/toolkit 0.9.4 → 0.9.5
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/dist/cli.js +31 -11
- package/dist/cockpit/menu.js +52 -12
- package/dist/prompts/index.js +7 -3
- package/dist/service-runtime-status.js +48 -0
- package/dist/templates.js +41 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { promptRepositoryUrl } from './prompt-repositories.js';
|
|
|
22
22
|
import { inferGitHubOwner, inferRepoPath, normalizeRepositoryUrl } from './repo-url.js';
|
|
23
23
|
import { addModuleRepo, listModuleRepos, removeModuleRepo } from './repo-actions.js';
|
|
24
24
|
import { renderSafeResetPreview, safeResetEnvironment } from './safe-reset.js';
|
|
25
|
+
import { getServiceRuntimeStatus, renderServiceRuntimeStatusLine, } from './service-runtime-status.js';
|
|
25
26
|
import { listSources, renderSourceList, sourceListJson, sourceSyncJson, syncSources, } from './source-actions.js';
|
|
26
27
|
import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget, renderExistingEnvironmentSummary, renderForeignEnvironmentTargetWarning, } from './environment-target-preflight.js';
|
|
27
28
|
import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
|
|
@@ -195,12 +196,16 @@ function renderStartupBanner(details, latestVersion) {
|
|
|
195
196
|
const versionLine = startupVersionLine(latestVersion);
|
|
196
197
|
return renderBanner(details?.(versionLine), details ? { version: versionLine } : undefined);
|
|
197
198
|
}
|
|
198
|
-
function renderCockpitStatusLines(status, lastStatus) {
|
|
199
|
-
return [renderStartupEnvironmentLine(status), lastStatus];
|
|
199
|
+
function renderCockpitStatusLines(status, serviceStatus, lastStatus) {
|
|
200
|
+
return [renderStartupEnvironmentLine(status), renderServiceRuntimeStatusLine(serviceStatus), lastStatus];
|
|
200
201
|
}
|
|
201
202
|
function renderLastCommandStatus(command) {
|
|
202
203
|
return `Last: ${command.label} ✓ completed`;
|
|
203
204
|
}
|
|
205
|
+
function renderLastCommandError(command, error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
return `Last: ${command.label} ✗ Error: ${message}`;
|
|
208
|
+
}
|
|
204
209
|
function clearCockpitScreen() {
|
|
205
210
|
if (process.stdout.isTTY) {
|
|
206
211
|
process.stdout.write('\u001B[2J\u001B[H');
|
|
@@ -245,8 +250,8 @@ async function showStartup(argv, skipUpdateCheck, details) {
|
|
|
245
250
|
}
|
|
246
251
|
console.log();
|
|
247
252
|
}
|
|
248
|
-
async function selectCockpitCommandFromMenu() {
|
|
249
|
-
const selection = await selectCockpitTopLevelMenu();
|
|
253
|
+
async function selectCockpitCommandFromMenu(serviceStatus) {
|
|
254
|
+
const selection = await selectCockpitTopLevelMenu({ serviceStatus });
|
|
250
255
|
if (selection.kind === 'exit') {
|
|
251
256
|
return 'exit';
|
|
252
257
|
}
|
|
@@ -1050,22 +1055,37 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
|
|
|
1050
1055
|
return;
|
|
1051
1056
|
}
|
|
1052
1057
|
let lastStatus = 'Last: Ready';
|
|
1053
|
-
|
|
1054
|
-
|
|
1058
|
+
let status = await getEnvironmentStatus(cwd);
|
|
1059
|
+
let serviceStatus = await getServiceRuntimeStatus(cwd, status);
|
|
1060
|
+
await showStartup(argv, skipUpdateCheck, () => renderCockpitStatusLines(status, serviceStatus, lastStatus));
|
|
1055
1061
|
while (true) {
|
|
1056
1062
|
try {
|
|
1057
|
-
const command = await selectCockpitCommandFromMenu();
|
|
1063
|
+
const command = await selectCockpitCommandFromMenu(serviceStatus);
|
|
1058
1064
|
if (command === 'exit') {
|
|
1059
1065
|
return;
|
|
1060
1066
|
}
|
|
1061
|
-
|
|
1067
|
+
let outcome = 'continue';
|
|
1068
|
+
let commandFailed = false;
|
|
1069
|
+
try {
|
|
1070
|
+
outcome = await runCockpitCommand(command, cwd);
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
if (isMenuBackSignal(error)) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
commandFailed = true;
|
|
1077
|
+
lastStatus = renderLastCommandError(command, error);
|
|
1078
|
+
}
|
|
1062
1079
|
if (outcome === 'exit') {
|
|
1063
1080
|
return;
|
|
1064
1081
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1082
|
+
if (!commandFailed) {
|
|
1083
|
+
lastStatus = renderLastCommandStatus(command);
|
|
1084
|
+
}
|
|
1085
|
+
status = await getEnvironmentStatus(cwd);
|
|
1086
|
+
serviceStatus = await getServiceRuntimeStatus(cwd, status);
|
|
1067
1087
|
clearCockpitScreen();
|
|
1068
|
-
console.log(renderBanner(renderCockpitStatusLines(status, lastStatus), { version: startupVersionLine() }));
|
|
1088
|
+
console.log(renderBanner(renderCockpitStatusLines(status, serviceStatus, lastStatus), { version: startupVersionLine() }));
|
|
1069
1089
|
console.log();
|
|
1070
1090
|
}
|
|
1071
1091
|
catch (error) {
|
package/dist/cockpit/menu.js
CHANGED
|
@@ -32,25 +32,51 @@ function categoryHeading(category) {
|
|
|
32
32
|
function commandName(command) {
|
|
33
33
|
return `${rgb(226, 184, 96, ` ${command.label.padEnd(topLevelCommandLabelWidth)}`)}${dim(` ${command.description}`)}`;
|
|
34
34
|
}
|
|
35
|
-
function
|
|
35
|
+
function disabledReason(command, serviceStatus) {
|
|
36
|
+
if (command.category !== 'services' || !serviceStatus)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (serviceStatus.kind === 'docker-not-running')
|
|
39
|
+
return 'Docker not running.';
|
|
40
|
+
if (serviceStatus.kind === 'running' && command.id === 'start')
|
|
41
|
+
return 'Already running.';
|
|
42
|
+
if (serviceStatus.kind === 'stopped' && ['stop', 'restart', 'logs', 'shell'].includes(command.id)) {
|
|
43
|
+
return 'Services stopped.';
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function disabledMenuReason(serviceStatus) {
|
|
48
|
+
if (serviceStatus?.kind === 'docker-not-running')
|
|
49
|
+
return 'Docker not running.';
|
|
50
|
+
if (serviceStatus?.kind === 'running')
|
|
51
|
+
return 'Already running.';
|
|
52
|
+
if (serviceStatus?.kind === 'stopped')
|
|
53
|
+
return 'Services stopped.';
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function disabledError(serviceStatus) {
|
|
57
|
+
const reason = disabledMenuReason(serviceStatus);
|
|
58
|
+
return reason ? `This option is disabled and cannot be selected.\nReason: ${reason}` : undefined;
|
|
59
|
+
}
|
|
60
|
+
function categoryChoices(category, index, serviceStatus) {
|
|
36
61
|
const choices = [
|
|
37
62
|
promptSeparator(categoryHeading(category)),
|
|
38
63
|
...topLevelCommands
|
|
39
64
|
.filter((command) => command.category === category)
|
|
40
|
-
.map((command) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
.map((command) => {
|
|
66
|
+
const reason = disabledReason(command, serviceStatus);
|
|
67
|
+
return {
|
|
68
|
+
value: command,
|
|
69
|
+
name: commandName(command),
|
|
70
|
+
short: command.label,
|
|
71
|
+
disabled: reason ? true : undefined,
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
45
74
|
];
|
|
46
75
|
if (index < topLevelCategoryOrder.length - 1) {
|
|
47
76
|
choices.push(promptSeparator(' '));
|
|
48
77
|
}
|
|
49
78
|
return choices;
|
|
50
79
|
}
|
|
51
|
-
const topLevelChoices = [
|
|
52
|
-
...topLevelCategoryOrder.flatMap(categoryChoices),
|
|
53
|
-
];
|
|
54
80
|
const minimumTopLevelPageSize = 8;
|
|
55
81
|
const startupViewportReservedRows = 11;
|
|
56
82
|
function topLevelPageSize(choiceCount) {
|
|
@@ -75,15 +101,29 @@ function menuDeps(deps = {}) {
|
|
|
75
101
|
function isCockpitCommand(value) {
|
|
76
102
|
return typeof value === 'object' && value !== null && 'id' in value && 'slashAlias' in value;
|
|
77
103
|
}
|
|
104
|
+
function topLevelChoices(serviceStatus) {
|
|
105
|
+
return topLevelCategoryOrder.flatMap((category, index) => categoryChoices(category, index, serviceStatus));
|
|
106
|
+
}
|
|
107
|
+
function defaultCommand(serviceStatus) {
|
|
108
|
+
if (serviceStatus?.kind === 'running') {
|
|
109
|
+
return cockpitCommands.find((command) => command.id === 'stop') ?? topLevelCommands[0];
|
|
110
|
+
}
|
|
111
|
+
if (serviceStatus?.kind === 'docker-not-running') {
|
|
112
|
+
return cockpitCommands.find((command) => command.id === 'status') ?? topLevelCommands[0];
|
|
113
|
+
}
|
|
114
|
+
return cockpitCommands.find((command) => command.id === 'start') ?? topLevelCommands[0];
|
|
115
|
+
}
|
|
78
116
|
export async function selectCockpitTopLevelMenu(options = {}) {
|
|
79
117
|
const deps = menuDeps(options);
|
|
118
|
+
const choices = topLevelChoices(options.serviceStatus);
|
|
80
119
|
const selected = await deps.select({
|
|
81
120
|
message: '',
|
|
82
|
-
choices: [...
|
|
83
|
-
default:
|
|
84
|
-
pageSize: topLevelPageSize(
|
|
121
|
+
choices: [...choices],
|
|
122
|
+
default: defaultCommand(options.serviceStatus),
|
|
123
|
+
pageSize: topLevelPageSize(choices.length),
|
|
85
124
|
loop: false,
|
|
86
125
|
hideMessage: true,
|
|
126
|
+
disabledError: disabledError(options.serviceStatus),
|
|
87
127
|
});
|
|
88
128
|
deps.handleCancel(selected, 'exit');
|
|
89
129
|
if (selected === 'exit') {
|
package/dist/prompts/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitKeypressEvents } from 'node:readline';
|
|
2
|
+
import { styleText } from 'node:util';
|
|
2
3
|
import inquirerSelect, { Separator as InquirerSeparator } from '@inquirer/select';
|
|
3
4
|
import inquirerSearch from '@inquirer/search';
|
|
4
5
|
import { confirm as inquirerConfirm, input as inquirerInput } from '@inquirer/prompts';
|
|
@@ -90,9 +91,10 @@ function asInquirerSelectConfig(options) {
|
|
|
90
91
|
pageSize: options.pageSize,
|
|
91
92
|
loop: options.loop,
|
|
92
93
|
hideMessage: options.hideMessage,
|
|
94
|
+
disabledError: options.disabledError,
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
|
-
function hiddenSelectTheme() {
|
|
97
|
+
function hiddenSelectTheme(disabledError) {
|
|
96
98
|
return {
|
|
97
99
|
prefix: '',
|
|
98
100
|
icon: {
|
|
@@ -101,19 +103,21 @@ function hiddenSelectTheme() {
|
|
|
101
103
|
style: {
|
|
102
104
|
message: () => '',
|
|
103
105
|
highlight: (text) => text,
|
|
106
|
+
disabled: (text) => styleText('dim', text.replace(/ \(disabled\)$/u, ''), { validateStream: false }),
|
|
104
107
|
keysHelpTip: () => '↑↓ navigate • ⏎ select • Ctrl+C exit',
|
|
105
108
|
},
|
|
109
|
+
i18n: disabledError ? { disabledError } : undefined,
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
112
|
function withHiddenSelectMessage(config) {
|
|
109
113
|
if (!config.hideMessage) {
|
|
110
114
|
return config;
|
|
111
115
|
}
|
|
112
|
-
const { hideMessage: _hideMessage, ...inquirerConfig } = config;
|
|
116
|
+
const { disabledError, hideMessage: _hideMessage, ...inquirerConfig } = config;
|
|
113
117
|
return {
|
|
114
118
|
...inquirerConfig,
|
|
115
119
|
message: '',
|
|
116
|
-
theme: hiddenSelectTheme(),
|
|
120
|
+
theme: hiddenSelectTheme(disabledError),
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
function asInquirerConfirmConfig(options) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
function run(command, args, options) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
execFile(command, args, { cwd: options.cwd }, (error, stdout) => {
|
|
5
|
+
if (error) {
|
|
6
|
+
reject(error);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
resolve({ stdout });
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function renderServiceRuntimeStatusLine(status) {
|
|
14
|
+
if (status.kind === 'running')
|
|
15
|
+
return 'Status: ● Services running';
|
|
16
|
+
if (status.kind === 'docker-not-running')
|
|
17
|
+
return 'Status: ● Docker not running';
|
|
18
|
+
return 'Status: ● Services stopped';
|
|
19
|
+
}
|
|
20
|
+
export async function getServiceRuntimeStatus(target, environmentStatus, runner = run) {
|
|
21
|
+
try {
|
|
22
|
+
await runner('docker', ['info', '--format', '{{.ServerVersion}}'], { cwd: target });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { kind: 'docker-not-running' };
|
|
26
|
+
}
|
|
27
|
+
if (environmentStatus.kind !== 'environment' ||
|
|
28
|
+
environmentStatus.composeFiles.length === 0 ||
|
|
29
|
+
environmentStatus.composeErrors.length > 0) {
|
|
30
|
+
return { kind: 'stopped' };
|
|
31
|
+
}
|
|
32
|
+
const args = [
|
|
33
|
+
'compose',
|
|
34
|
+
...environmentStatus.composeFiles.flatMap((file) => ['-f', file]),
|
|
35
|
+
'ps',
|
|
36
|
+
'--services',
|
|
37
|
+
'--filter',
|
|
38
|
+
'status=running',
|
|
39
|
+
];
|
|
40
|
+
let result;
|
|
41
|
+
try {
|
|
42
|
+
result = await runner('docker', args, { cwd: target });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { kind: 'stopped' };
|
|
46
|
+
}
|
|
47
|
+
return result.stdout.trim() ? { kind: 'running' } : { kind: 'stopped' };
|
|
48
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -279,6 +279,10 @@ const ANSI_DIM = '\u001B[2m';
|
|
|
279
279
|
const ANSI_INFO = '\u001B[38;2;139;166;190m';
|
|
280
280
|
const ANSI_TAGLINE = '\u001B[38;2;120;157;181m';
|
|
281
281
|
const ANSI_META = '\u001B[38;2;218;236;246m';
|
|
282
|
+
const ANSI_SUCCESS = '\u001B[32m';
|
|
283
|
+
const ANSI_ERROR = '\u001B[31m';
|
|
284
|
+
const ANSI_WARNING = '\u001B[33m';
|
|
285
|
+
const ANSI_DEFAULT_FOREGROUND = '\u001B[39m';
|
|
282
286
|
const ANSI_RESET = '\u001B[0m';
|
|
283
287
|
const BANNER_TAGLINE = 'Development, staging and production workflows for Odoo projects.';
|
|
284
288
|
function gradientColor(column, width) {
|
|
@@ -307,15 +311,50 @@ function renderDimInfo(value) {
|
|
|
307
311
|
function renderMetaInfo(value) {
|
|
308
312
|
return `${ANSI_META}${value}${ANSI_RESET}`;
|
|
309
313
|
}
|
|
314
|
+
function renderSuccessInfo(value) {
|
|
315
|
+
return `${ANSI_SUCCESS}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
316
|
+
}
|
|
317
|
+
function renderErrorInfo(value) {
|
|
318
|
+
return `${ANSI_ERROR}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
319
|
+
}
|
|
320
|
+
function renderWarningInfo(value) {
|
|
321
|
+
return `${ANSI_WARNING}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
322
|
+
}
|
|
310
323
|
function renderTaglineInfo(value) {
|
|
311
324
|
return `${ANSI_TAGLINE}${value}${ANSI_RESET}`;
|
|
312
325
|
}
|
|
313
326
|
function renderBannerDetail(value) {
|
|
314
|
-
const match = /^(Environment|Last):(.*)$/u.exec(value);
|
|
327
|
+
const match = /^(Environment|Status|Last):(.*)$/u.exec(value);
|
|
315
328
|
if (!match) {
|
|
316
329
|
return renderDimInfo(value);
|
|
317
330
|
}
|
|
318
|
-
|
|
331
|
+
const label = match[1];
|
|
332
|
+
const detail = match[2] ?? '';
|
|
333
|
+
if (label === 'Status') {
|
|
334
|
+
const statusMatch = /^ (●) (.*)$/u.exec(detail);
|
|
335
|
+
if (statusMatch) {
|
|
336
|
+
const marker = statusMatch[1] ?? '';
|
|
337
|
+
const message = statusMatch[2] ?? '';
|
|
338
|
+
const renderMarker = message === 'Services running' ? renderSuccessInfo : renderWarningInfo;
|
|
339
|
+
return `${renderMetaInfo(`${label}:`)} ${renderMarker(marker)}${renderTaglineInfo(` ${message}`)}`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (label === 'Last') {
|
|
343
|
+
const completedMatch = /^(.*?)( ✓ completed)$/u.exec(detail);
|
|
344
|
+
if (completedMatch) {
|
|
345
|
+
return `${renderMetaInfo(`${label}:`)}${renderDimInfo(completedMatch[1] ?? '')}${renderSuccessInfo(completedMatch[2] ?? '')}`;
|
|
346
|
+
}
|
|
347
|
+
const errorMatch = /^(.*?)( ✗ Error)(: .*)?$/u.exec(detail);
|
|
348
|
+
if (errorMatch) {
|
|
349
|
+
return [
|
|
350
|
+
renderMetaInfo(`${label}:`),
|
|
351
|
+
renderDimInfo(errorMatch[1] ?? ''),
|
|
352
|
+
renderErrorInfo(errorMatch[2] ?? ''),
|
|
353
|
+
renderTaglineInfo(errorMatch[3] ?? ''),
|
|
354
|
+
].join('');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return `${renderMetaInfo(`${label}:`)}${renderDimInfo(detail)}`;
|
|
319
358
|
}
|
|
320
359
|
export function renderBanner(details = [], options = {}) {
|
|
321
360
|
const title = `${applyBannerGradient('WPMoo Toolkit')}${options.version ? ` ${renderDimInfo(options.version)}` : ''}`;
|