@wpmoo/toolkit 0.9.5 → 0.9.7
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/README.md +95 -443
- package/dist/cli.js +417 -35
- package/dist/cockpit/command-registry.js +5 -1
- package/dist/cockpit/daily-prompts.js +30 -11
- package/dist/cockpit/menu.js +4 -1
- package/dist/cockpit/module-action-menu.js +40 -0
- package/dist/cockpit/module-browser.js +117 -0
- package/dist/daily-actions.js +40 -3
- package/dist/databases.js +58 -0
- package/dist/help.js +4 -2
- package/dist/menu-navigation.js +2 -2
- package/dist/module-actions.js +50 -1
- package/dist/prompts/index.js +69 -18
- package/dist/system-prerequisites.js +189 -0
- package/dist/templates.js +44 -25
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
const minimumNodeVersion = '20.17.0';
|
|
3
|
+
const ANSI_RESET = '\u001B[0m';
|
|
4
|
+
const ANSI_DEFAULT_FOREGROUND = '\u001B[39m';
|
|
5
|
+
const ANSI_DIM_YELLOW = '\u001B[2m\u001B[38;2;226;184;96m';
|
|
6
|
+
const ANSI_STRONG_YELLOW = '\u001B[38;2;226;184;96m';
|
|
7
|
+
const ANSI_CYAN = '\u001B[36m';
|
|
8
|
+
const ANSI_GREEN = '\u001B[32m';
|
|
9
|
+
const ANSI_LIGHT_GREEN = '\u001B[38;2;125;231;152m';
|
|
10
|
+
const ANSI_RED = '\u001B[38;2;224;92;120m';
|
|
11
|
+
const ANSI_DIM = '\u001B[2m';
|
|
12
|
+
export const realSystemCommandRunner = (command, args) => new Promise((resolve, reject) => {
|
|
13
|
+
execFile(command, args, { windowsHide: true }, (error, stdout, stderr) => {
|
|
14
|
+
if (error) {
|
|
15
|
+
reject(error);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
resolve({ stdout, stderr });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
function parseVersionParts(value) {
|
|
22
|
+
const [major = '0', minor = '0', patch = '0'] = value.replace(/^v/u, '').split('.');
|
|
23
|
+
return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
|
|
24
|
+
}
|
|
25
|
+
function isNodeVersionSupported(version) {
|
|
26
|
+
const current = parseVersionParts(version);
|
|
27
|
+
const minimum = parseVersionParts(minimumNodeVersion);
|
|
28
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
29
|
+
if (current[index] > minimum[index])
|
|
30
|
+
return true;
|
|
31
|
+
if (current[index] < minimum[index])
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function forcedMissingTools(env) {
|
|
37
|
+
return new Set((env.WPMOO_TEST_MISSING_TOOLS ?? '')
|
|
38
|
+
.split(/[,\s]+/u)
|
|
39
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
40
|
+
.filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
function issueForCheck(check) {
|
|
43
|
+
if (check.status === 'found')
|
|
44
|
+
return undefined;
|
|
45
|
+
return { tool: check.tool, reason: check.status };
|
|
46
|
+
}
|
|
47
|
+
async function checkCommand(runner, tool, label, command, args, forcedMissing, missingAlias = tool) {
|
|
48
|
+
if (forcedMissing.has(tool) || forcedMissing.has(missingAlias)) {
|
|
49
|
+
return { tool, label, status: 'missing' };
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = await runner(command, args);
|
|
53
|
+
return {
|
|
54
|
+
tool,
|
|
55
|
+
label,
|
|
56
|
+
status: 'found',
|
|
57
|
+
detail: result.stdout.trim() || undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { tool, label, status: 'missing' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function checkDockerEngine(runner, forcedMissing) {
|
|
65
|
+
if (forcedMissing.has('docker-engine')) {
|
|
66
|
+
return { tool: 'docker-engine', label: 'Docker Engine', status: 'not-running' };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const result = await runner('docker', ['info', '--format', '{{.ServerVersion}}']);
|
|
70
|
+
return {
|
|
71
|
+
tool: 'docker-engine',
|
|
72
|
+
label: 'Docker Engine',
|
|
73
|
+
status: 'found',
|
|
74
|
+
detail: result.stdout.trim() || undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { tool: 'docker-engine', label: 'Docker Engine', status: 'not-running' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function getSystemPrerequisiteStatus(options = {}) {
|
|
82
|
+
const runner = options.runner ?? realSystemCommandRunner;
|
|
83
|
+
const env = options.env ?? process.env;
|
|
84
|
+
const forcedMissing = forcedMissingTools(env);
|
|
85
|
+
const checks = [];
|
|
86
|
+
const nodeVersion = options.nodeVersion ?? process.versions.node;
|
|
87
|
+
checks.push({
|
|
88
|
+
tool: 'node',
|
|
89
|
+
label: 'Node.js 20+',
|
|
90
|
+
status: isNodeVersionSupported(nodeVersion) ? 'found' : 'unsupported-version',
|
|
91
|
+
detail: `v${nodeVersion}`,
|
|
92
|
+
});
|
|
93
|
+
checks.push(await checkCommand(runner, 'git', 'Git', 'git', ['--version'], forcedMissing));
|
|
94
|
+
const dockerCheck = await checkCommand(runner, 'docker', 'Docker Desktop', 'docker', ['--version'], forcedMissing, 'docker-desktop');
|
|
95
|
+
checks.push(dockerCheck);
|
|
96
|
+
if (dockerCheck.status === 'found') {
|
|
97
|
+
checks.push(await checkCommand(runner, 'docker-compose', 'Docker Compose', 'docker', ['compose', 'version'], forcedMissing, 'compose'));
|
|
98
|
+
checks.push(await checkDockerEngine(runner, forcedMissing));
|
|
99
|
+
}
|
|
100
|
+
const issues = checks.map(issueForCheck).filter((issue) => Boolean(issue));
|
|
101
|
+
return {
|
|
102
|
+
ok: issues.length === 0,
|
|
103
|
+
checks,
|
|
104
|
+
issues,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function statusLabel(check) {
|
|
108
|
+
if (check.status === 'found')
|
|
109
|
+
return 'ok';
|
|
110
|
+
if (check.status === 'not-running')
|
|
111
|
+
return 'Not running';
|
|
112
|
+
if (check.status === 'unsupported-version')
|
|
113
|
+
return 'Unsupported version';
|
|
114
|
+
return 'Missing';
|
|
115
|
+
}
|
|
116
|
+
function hasIssue(status, tool) {
|
|
117
|
+
return status.issues.some((issue) => issue.tool === tool);
|
|
118
|
+
}
|
|
119
|
+
function supportsAnsi() {
|
|
120
|
+
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
|
|
121
|
+
}
|
|
122
|
+
function ansi(value, open, close = ANSI_DEFAULT_FOREGROUND) {
|
|
123
|
+
if (!supportsAnsi())
|
|
124
|
+
return value;
|
|
125
|
+
return `${open}${value}${close}`;
|
|
126
|
+
}
|
|
127
|
+
function dim(value) {
|
|
128
|
+
return ansi(value, ANSI_DIM, ANSI_RESET);
|
|
129
|
+
}
|
|
130
|
+
function cyan(value) {
|
|
131
|
+
return ansi(value, ANSI_CYAN);
|
|
132
|
+
}
|
|
133
|
+
function green(value) {
|
|
134
|
+
return ansi(value, ANSI_GREEN);
|
|
135
|
+
}
|
|
136
|
+
function red(value) {
|
|
137
|
+
return ansi(value, ANSI_RED);
|
|
138
|
+
}
|
|
139
|
+
function mutedWarning(value) {
|
|
140
|
+
return ansi(value, ANSI_DIM_YELLOW, ANSI_RESET);
|
|
141
|
+
}
|
|
142
|
+
function yellow(value) {
|
|
143
|
+
return ansi(value, ANSI_STRONG_YELLOW);
|
|
144
|
+
}
|
|
145
|
+
function okText() {
|
|
146
|
+
return ansi('ok', ANSI_LIGHT_GREEN);
|
|
147
|
+
}
|
|
148
|
+
function downloadUrlForCheck(check) {
|
|
149
|
+
if (check.status === 'found') {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
if (check.tool === 'node') {
|
|
153
|
+
return 'https://nodejs.org/en/download';
|
|
154
|
+
}
|
|
155
|
+
if (check.tool === 'git') {
|
|
156
|
+
return 'https://git-scm.com/downloads';
|
|
157
|
+
}
|
|
158
|
+
if (check.tool === 'docker' || check.tool === 'docker-compose') {
|
|
159
|
+
return 'https://www.docker.com/products/docker-desktop/';
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function renderStatusLine(check) {
|
|
164
|
+
const symbol = check.status === 'found' ? green('✓') : red('✕');
|
|
165
|
+
const url = downloadUrlForCheck(check);
|
|
166
|
+
const status = check.status === 'found' ? okText() : url ? `${yellow('↗')} ${cyan(url)}` : dim(statusLabel(check));
|
|
167
|
+
return `${symbol} ${cyan(check.label.padEnd(18))} ${status}`;
|
|
168
|
+
}
|
|
169
|
+
export function renderSystemPrerequisiteGuidance(status) {
|
|
170
|
+
if (status.ok) {
|
|
171
|
+
return 'All required system prerequisites are available.';
|
|
172
|
+
}
|
|
173
|
+
const lines = [
|
|
174
|
+
'Required tools before environment setup starts',
|
|
175
|
+
'',
|
|
176
|
+
...status.checks.map(renderStatusLine),
|
|
177
|
+
'',
|
|
178
|
+
];
|
|
179
|
+
if (hasIssue(status, 'docker-engine')) {
|
|
180
|
+
lines.push(mutedWarning('Docker Desktop is installed, but Docker Engine is not running.'));
|
|
181
|
+
lines.push(mutedWarning('Start Docker Desktop, then check again.'));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
lines.push(mutedWarning('Environment setup has not started yet.'));
|
|
185
|
+
lines.push(mutedWarning('Install the missing tools, restart your terminal if PATH changed,'));
|
|
186
|
+
lines.push(mutedWarning('start Docker Desktop, then run WPMoo Toolkit again.'));
|
|
187
|
+
}
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -285,6 +285,9 @@ const ANSI_WARNING = '\u001B[33m';
|
|
|
285
285
|
const ANSI_DEFAULT_FOREGROUND = '\u001B[39m';
|
|
286
286
|
const ANSI_RESET = '\u001B[0m';
|
|
287
287
|
const BANNER_TAGLINE = 'Development, staging and production workflows for Odoo projects.';
|
|
288
|
+
function shouldRenderBannerColor(options) {
|
|
289
|
+
return options.color ?? process.env.NO_COLOR === undefined;
|
|
290
|
+
}
|
|
288
291
|
function gradientColor(column, width) {
|
|
289
292
|
const ratio = width <= 1 ? 0 : column / (width - 1);
|
|
290
293
|
const [startR, startG, startB] = BANNER_GRADIENT_START;
|
|
@@ -294,7 +297,10 @@ function gradientColor(column, width) {
|
|
|
294
297
|
const b = Math.round(startB + (endB - startB) * ratio);
|
|
295
298
|
return `\u001B[38;2;${r};${g};${b}m`;
|
|
296
299
|
}
|
|
297
|
-
function applyBannerGradient(banner) {
|
|
300
|
+
function applyBannerGradient(banner, color) {
|
|
301
|
+
if (!color) {
|
|
302
|
+
return banner;
|
|
303
|
+
}
|
|
298
304
|
const lines = banner.split('\n');
|
|
299
305
|
return lines
|
|
300
306
|
.map((line) => {
|
|
@@ -305,28 +311,40 @@ function applyBannerGradient(banner) {
|
|
|
305
311
|
})
|
|
306
312
|
.join('\n');
|
|
307
313
|
}
|
|
308
|
-
function renderDimInfo(value) {
|
|
314
|
+
function renderDimInfo(value, color) {
|
|
315
|
+
if (!color)
|
|
316
|
+
return value;
|
|
309
317
|
return `${ANSI_DIM}${ANSI_INFO}${value}${ANSI_RESET}`;
|
|
310
318
|
}
|
|
311
|
-
function renderMetaInfo(value) {
|
|
319
|
+
function renderMetaInfo(value, color) {
|
|
320
|
+
if (!color)
|
|
321
|
+
return value;
|
|
312
322
|
return `${ANSI_META}${value}${ANSI_RESET}`;
|
|
313
323
|
}
|
|
314
|
-
function renderSuccessInfo(value) {
|
|
324
|
+
function renderSuccessInfo(value, color) {
|
|
325
|
+
if (!color)
|
|
326
|
+
return value;
|
|
315
327
|
return `${ANSI_SUCCESS}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
316
328
|
}
|
|
317
|
-
function renderErrorInfo(value) {
|
|
329
|
+
function renderErrorInfo(value, color) {
|
|
330
|
+
if (!color)
|
|
331
|
+
return value;
|
|
318
332
|
return `${ANSI_ERROR}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
319
333
|
}
|
|
320
|
-
function renderWarningInfo(value) {
|
|
334
|
+
function renderWarningInfo(value, color) {
|
|
335
|
+
if (!color)
|
|
336
|
+
return value;
|
|
321
337
|
return `${ANSI_WARNING}${value}${ANSI_DEFAULT_FOREGROUND}`;
|
|
322
338
|
}
|
|
323
|
-
function renderTaglineInfo(value) {
|
|
339
|
+
function renderTaglineInfo(value, color) {
|
|
340
|
+
if (!color)
|
|
341
|
+
return value;
|
|
324
342
|
return `${ANSI_TAGLINE}${value}${ANSI_RESET}`;
|
|
325
343
|
}
|
|
326
|
-
function renderBannerDetail(value) {
|
|
344
|
+
function renderBannerDetail(value, color) {
|
|
327
345
|
const match = /^(Environment|Status|Last):(.*)$/u.exec(value);
|
|
328
346
|
if (!match) {
|
|
329
|
-
return renderDimInfo(value);
|
|
347
|
+
return renderDimInfo(value, color);
|
|
330
348
|
}
|
|
331
349
|
const label = match[1];
|
|
332
350
|
const detail = match[2] ?? '';
|
|
@@ -336,36 +354,37 @@ function renderBannerDetail(value) {
|
|
|
336
354
|
const marker = statusMatch[1] ?? '';
|
|
337
355
|
const message = statusMatch[2] ?? '';
|
|
338
356
|
const renderMarker = message === 'Services running' ? renderSuccessInfo : renderWarningInfo;
|
|
339
|
-
return `${renderMetaInfo(`${label}
|
|
357
|
+
return `${renderMetaInfo(`${label}:`, color)} ${renderMarker(marker, color)}${renderTaglineInfo(` ${message}`, color)}`;
|
|
340
358
|
}
|
|
341
359
|
}
|
|
342
360
|
if (label === 'Last') {
|
|
343
361
|
const completedMatch = /^(.*?)( ✓ completed)$/u.exec(detail);
|
|
344
362
|
if (completedMatch) {
|
|
345
|
-
return `${renderMetaInfo(`${label}
|
|
363
|
+
return `${renderMetaInfo(`${label}:`, color)}${renderDimInfo(completedMatch[1] ?? '', color)}${renderSuccessInfo(completedMatch[2] ?? '', color)}`;
|
|
346
364
|
}
|
|
347
365
|
const errorMatch = /^(.*?)( ✗ Error)(: .*)?$/u.exec(detail);
|
|
348
366
|
if (errorMatch) {
|
|
349
367
|
return [
|
|
350
|
-
renderMetaInfo(`${label}
|
|
351
|
-
renderDimInfo(errorMatch[1] ?? ''),
|
|
352
|
-
renderErrorInfo(errorMatch[2] ?? ''),
|
|
353
|
-
renderTaglineInfo(errorMatch[3] ?? ''),
|
|
368
|
+
renderMetaInfo(`${label}:`, color),
|
|
369
|
+
renderDimInfo(errorMatch[1] ?? '', color),
|
|
370
|
+
renderErrorInfo(errorMatch[2] ?? '', color),
|
|
371
|
+
renderTaglineInfo(errorMatch[3] ?? '', color),
|
|
354
372
|
].join('');
|
|
355
373
|
}
|
|
356
374
|
}
|
|
357
|
-
return `${renderMetaInfo(`${label}
|
|
375
|
+
return `${renderMetaInfo(`${label}:`, color)}${renderDimInfo(detail, color)}`;
|
|
358
376
|
}
|
|
359
377
|
export function renderBanner(details = [], options = {}) {
|
|
360
|
-
const
|
|
378
|
+
const color = shouldRenderBannerColor(options);
|
|
379
|
+
const title = `${applyBannerGradient('WPMoo Toolkit', color)}${options.version ? ` ${renderDimInfo(options.version, color)}` : ''}`;
|
|
361
380
|
const header = [
|
|
362
381
|
title,
|
|
363
|
-
applyBannerGradient('Workflow Platform · Micro Object Oriented'),
|
|
364
|
-
renderTaglineInfo(BANNER_TAGLINE),
|
|
365
|
-
applyBannerGradient('━'.repeat(BANNER_TAGLINE.length)),
|
|
382
|
+
applyBannerGradient('Workflow Platform · Micro Object Oriented', color),
|
|
383
|
+
renderTaglineInfo(BANNER_TAGLINE, color),
|
|
384
|
+
applyBannerGradient('━'.repeat(BANNER_TAGLINE.length), color),
|
|
366
385
|
].join('\n');
|
|
367
|
-
const detailsBlock = details.length > 0 ? `\n${details.map((line) => renderBannerDetail(line)).join('\n')}` : '';
|
|
368
|
-
return `\n${ANSI_BOLD}${header}${ANSI_RESET}${detailsBlock}`;
|
|
386
|
+
const detailsBlock = details.length > 0 ? `\n${details.map((line) => renderBannerDetail(line, color)).join('\n')}` : '';
|
|
387
|
+
return color ? `\n${ANSI_BOLD}${header}${ANSI_RESET}${detailsBlock}` : `\n${header}${detailsBlock}`;
|
|
369
388
|
}
|
|
370
389
|
export function renderGitignore() {
|
|
371
390
|
return `# macOS/editor noise
|
|
@@ -431,7 +450,7 @@ usage() {
|
|
|
431
450
|
"psql") echo "Usage: ./moo psql [db]" ;;
|
|
432
451
|
"install") echo "Usage: ./moo install <module[,module]> [db]" ;;
|
|
433
452
|
"update") echo "Usage: ./moo update <module[,module]> [db]" ;;
|
|
434
|
-
"test") echo "Usage: ./moo test <module[,module]> [--db <db>] [--mode init|update] [--tags <tags>]" ;;
|
|
453
|
+
"test") echo "Usage: ./moo test <module[,module]> [--db <db>] [--mode auto|init|update] [--tags <tags>]" ;;
|
|
435
454
|
"resetdb") echo "Usage: ./moo resetdb [db] [module[,module]]" ;;
|
|
436
455
|
"snapshot") echo "Usage: ./moo snapshot [db] [snapshot-name]" ;;
|
|
437
456
|
"restore-snapshot") echo "Usage: ./moo restore-snapshot [--dry-run] <snapshot-name> [db]" ;;
|
|
@@ -506,8 +525,8 @@ validate_test_args() {
|
|
|
506
525
|
echo "Missing value for --mode" >&2
|
|
507
526
|
exit 2
|
|
508
527
|
fi
|
|
509
|
-
if [[ "$2" != "init" && "$2" != "update" ]]; then
|
|
510
|
-
echo "Invalid value for --mode: expected init or update" >&2
|
|
528
|
+
if [[ "$2" != "auto" && "$2" != "init" && "$2" != "update" ]]; then
|
|
529
|
+
echo "Invalid value for --mode: expected auto, init, or update" >&2
|
|
511
530
|
exit 2
|
|
512
531
|
fi
|
|
513
532
|
shift 2
|