@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.
@@ -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}:`)} ${renderMarker(marker)}${renderTaglineInfo(` ${message}`)}`;
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}:`)}${renderDimInfo(completedMatch[1] ?? '')}${renderSuccessInfo(completedMatch[2] ?? '')}`;
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}:`)}${renderDimInfo(detail)}`;
375
+ return `${renderMetaInfo(`${label}:`, color)}${renderDimInfo(detail, color)}`;
358
376
  }
359
377
  export function renderBanner(details = [], options = {}) {
360
- const title = `${applyBannerGradient('WPMoo Toolkit')}${options.version ? ` ${renderDimInfo(options.version)}` : ''}`;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/toolkit",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "WPMoo Toolkit for development, staging, and production lifecycle workflows.",
5
5
  "type": "module",
6
6
  "repository": {