@wpmoo/toolkit 0.9.6 → 0.9.8

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 CHANGED
@@ -1,373 +1,152 @@
1
1
  ![WPMoo Toolkit for Odoo development workflows](docs/assets/wpmoo-banner.png)
2
2
 
3
- [![CI](https://img.shields.io/github/actions/workflow/status/wpmoo-org/wpmoo-toolkit/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit/actions/workflows/ci.yml) [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit) [![npm](https://img.shields.io/npm/v/@wpmoo/toolkit?label=npm&logo=npm&style=flat-square&color=blue)](https://www.npmjs.com/package/@wpmoo/toolkit) [![coverage](https://img.shields.io/codecov/c/github/wpmoo-org/wpmoo-toolkit?branch=main&label=coverage&logo=codecov&style=flat-square&color=blue)](https://codecov.io/gh/wpmoo-org/wpmoo-toolkit) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE) [![WPMoo Toolkit](https://img.shields.io/badge/WPMoo-Tool-714B67?style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/cangir) [![Patreon](https://img.shields.io/badge/Patreon-Support-F96854?logo=patreon&logoColor=white&style=flat-square)](https://www.patreon.com/wpmoo)
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/wpmoo-org/wpmoo-toolkit/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit/actions/workflows/ci.yml) [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit) [![npm](https://img.shields.io/npm/v/@wpmoo/toolkit?label=npm&logo=npm&style=flat-square&color=blue)](https://www.npmjs.com/package/@wpmoo/toolkit) [![coverage](https://img.shields.io/codecov/c/github/wpmoo-org/wpmoo-toolkit?branch=main&label=coverage&logo=codecov&style=flat-square&color=blue)](https://codecov.io/gh/wpmoo-org/wpmoo-toolkit) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE) [![Odoo Tool](https://img.shields.io/badge/Odoo-Tool-714B67?style=flat-square)](https://github.com/wpmoo-org/wpmoo-toolkit) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/cangir) [![Patreon](https://img.shields.io/badge/Patreon-Support-F96854?logo=patreon&logoColor=white&style=flat-square)](https://www.patreon.com/wpmoo)
4
4
 
5
5
  # WPMoo Toolkit
6
6
 
7
- WPMoo Toolkit is a development-first CLI for creating and operating Docker Compose based environments for Odoo, with source repositories managed as Git submodules.
7
+ WPMoo Toolkit is a free, MIT-licensed CLI for creating and operating repeatable Docker Compose based Odoo development environments.
8
8
 
9
- It gives Odoo teams a repeatable environment layout, a guided cockpit for daily work, direct commands for automation, and recovery tools that refresh generated files without touching product source code.
9
+ It is built for the everyday moments that tend to slow Odoo teams down: setting up a clean environment, keeping source repositories in a known layout, starting services, updating modules, testing changes, taking snapshots, restoring a database, and recovering generated files without touching product source code.
10
10
 
11
11
  WPMoo Toolkit is an independent project and is not affiliated with, endorsed by, or sponsored by Odoo S.A. Odoo is a trademark of Odoo S.A.
12
12
 
13
- ## Development Status
13
+ ## Why WPMoo Exists
14
14
 
15
- > [!IMPORTANT]
16
- > **Pre-1.0 active development:** WPMoo Toolkit has not reached `1.0.0` yet. Until the `1.0.0` release, use it as a preview tool for evaluation, local trials, and feedback rather than a dependency for critical production workflows. Setup conventions and command behavior may still change between pre-1.0 releases.
15
+ Odoo development has good building blocks. Docker Compose is familiar. OCA conventions are strong. The wider ecosystem has helped many teams think more clearly about Odoo infrastructure.
16
+
17
+ What we kept missing was a smaller, local-first workflow tool.
17
18
 
18
- ## Why WPMoo Toolkit
19
+ We wanted a generated development repository that stayed boring and recoverable. We wanted product source code to live in its own Git repositories, not be mixed with disposable runtime files. We wanted a cockpit that remembered the daily Odoo tasks developers actually run, without asking everyone to become an infrastructure specialist just to update a module or restore a snapshot.
19
20
 
20
- - Create a local Odoo development environment from a dev repository and one or more source repositories.
21
- - Keep product source repositories under `odoo/custom/src/private`, `odoo/custom/src/oca`, or `odoo/custom/src/external` as Git submodules pinned to the selected Odoo branch.
22
- - Copy Docker Compose resources from the standalone `wpmoo-org/odoo-docker-compose` resource instead of embedding large runtime assets in the TypeScript package.
23
- - Optionally copy project-local Agent Skills from `wpmoo-org/odoo-skills` into generated environments.
24
- - Use either a guided terminal cockpit or direct CLI commands for the same lifecycle tasks.
21
+ WPMoo is that layer. It does not try to replace the whole ecosystem. It gives an Odoo team a practical starting point, a stable folder layout, and a safer daily workflow.
22
+
23
+ ## What It Solves
24
+
25
+ - Creates a repeatable Odoo development environment from a product name, Odoo version, and one or more source repositories.
26
+ - Keeps Odoo source repositories under `private`, `oca`, or `external` categories as Git submodules in `odoo/custom/src/`.
27
+ - Provides a guided terminal cockpit for services, modules, database work, diagnostics, repository management, and maintenance.
28
+ - Includes direct commands for automation and CI-friendly terminal workflows.
29
+ - Adds recovery tools such as `status`, `doctor`, `snapshot`, `restore-snapshot`, and safe reset.
30
+ - Refreshes generated environment files without deleting product source code.
31
+
32
+ ## Development Status
33
+
34
+ > [!IMPORTANT]
35
+ > WPMoo Toolkit is still pre-1.0. Use it for evaluation, local trials, and feedback before relying on it for critical production workflows. Setup conventions and command behavior may still change before `1.0.0`.
25
36
 
26
37
  ## Prerequisites
27
38
 
28
39
  - Node.js `20.17+`
29
40
  - Git
30
- - Docker + Docker Compose for generated environment runtime commands
31
- - For GitHub-connected setup, install and authenticate GitHub CLI:
41
+ - Docker and Docker Compose for generated environment runtime commands
42
+ - Optional: GitHub CLI (`gh`) when you want setup to inspect or create GitHub repositories
32
43
 
33
- ```bash
34
- brew install gh
35
- gh auth login
36
- ```
44
+ Before environment setup starts, WPMoo checks Git, Docker, Docker Compose, and the Docker Engine. If a required tool is missing, the wizard stops before asking setup questions, shows official download links inline with the missing tools, and lets you check again or exit with `Ctrl+C`. Install the missing tools, restart your terminal if PATH changed, start Docker Desktop, then run `npx @wpmoo/toolkit` again.
37
45
 
38
- GitHub CLI (`gh`) is optional.
39
- WPMoo uses `gh` to inspect source/dev repositories and to create missing repos during setup. It also uses repository inspection to detect existing non-empty dev repositories and avoid overwriting them; if you do not want GitHub checks, keep setup local-only.
46
+ ```bash
47
+ brew install gh
48
+ gh auth login
49
+ ```
40
50
 
41
- The wizard currently offers Odoo `19.0`, `18.0`, `17.0`, and `16.0`. Generated
42
- environments now use the compact compose layout (`compose.yaml` with
43
- `compose/<env>.yaml` overlays). Legacy root-level
44
- `docker-compose_<version>.yml` layouts are still supported for compatibility.
51
+ GitHub CLI (`gh`) is optional. WPMoo can run local-only and source repositories can be added later.
45
52
 
46
- ## Quick Start
53
+ ## Quick Setup
47
54
 
48
- Run the guided wizard from a workspace directory:
55
+ Run the guided wizard from the workspace where you keep Odoo projects:
49
56
 
50
57
  ```bash
51
58
  npx @wpmoo/toolkit
52
59
  ```
53
60
 
54
- Short alias:
61
+ Short aliases are also available:
55
62
 
56
63
  ```bash
57
64
  npx wpmoo
65
+ npx @wpmoo/odoo
66
+ npx @wpmoo/odoo-dev
58
67
  ```
59
68
 
60
- Legacy package paths `npx @wpmoo/odoo` and `npx @wpmoo/odoo-dev` remain
61
- available as compatibility redirects to `@wpmoo/toolkit`.
69
+ Legacy package paths `npx @wpmoo/odoo` and `npx @wpmoo/odoo-dev` remain available for compatibility.
62
70
 
63
- If the current directory is not already a WPMoo environment, the CLI opens the create flow. It asks for the product slug, Odoo version, and environment folder. Choose any environment folder; the default is `./<product>_dev`.
71
+ When the current directory is not already a WPMoo environment, the CLI opens the create flow. It asks for a product slug, Odoo version, and environment folder. The default environment folder is `./<product>_dev`.
64
72
 
65
- After folder selection, connect Git/GitHub to use repository URLs. Choose local-only setup to skip Git/GitHub connection and source repo prompts. Add source repositories later from the cockpit (`Repositories` -> `add-repo`) or `npx @wpmoo/toolkit add-repo`.
66
-
67
- For non-interactive usage with repository URLs:
68
-
69
- Direct `create` commands keep the existing repo URL options; use `--target <path>` to choose a custom folder.
70
-
71
- ```bash
72
- npx @wpmoo/toolkit create \
73
- --product odoo_sample_module \
74
- --odoo-version 19.0 \
75
- --dev-repo-url https://github.com/example-org/odoo_sample_module_dev.git \
76
- --source-repo-url https://github.com/example-org/odoo_sample_module.git \
77
- --init-empty-repos
78
- ```
73
+ Choose any environment folder; the default is `./<product>_dev`. Choose local-only setup to skip Git/GitHub connection and source repo prompts. Add source repositories later from the cockpit (`Repositories` -> `add-repo`) or `npx @wpmoo/toolkit add-repo`. Direct `create` commands keep the existing repo URL options; use `--target <path>` to choose a custom folder.
79
74
 
80
- Add multiple source repositories by repeating `--source-repo-url`:
75
+ After setup, enter the generated environment and open the cockpit:
81
76
 
82
77
  ```bash
83
- npx @wpmoo/toolkit create \
84
- --product odoo_sample_module \
85
- --dev-repo-url https://github.com/example-org/odoo_sample_module_dev.git \
86
- --source-repo-url https://github.com/example-org/odoo_sample_module.git \
87
- --source-addons odoo_sample_module,odoo_sample_module_portal \
88
- --source-repo-url git@github.com:example-org/odoo_sample_module_reports.git \
89
- --source-path odoo_sample_module_reports \
90
- --source-addons odoo_sample_module_reports
78
+ cd <product>_dev
79
+ ./moo
91
80
  ```
92
81
 
93
- Preview planned files and commands without writing:
82
+ For non-interactive setup:
94
83
 
95
84
  ```bash
96
85
  npx @wpmoo/toolkit create \
97
86
  --product odoo_sample_module \
87
+ --odoo-version 19.0 \
98
88
  --dev-repo-url https://github.com/example-org/odoo_sample_module_dev.git \
99
89
  --source-repo-url https://github.com/example-org/odoo_sample_module.git \
100
- --dry-run
101
- ```
102
-
103
- ## The Cockpit
104
-
105
- Run the package with no command inside a generated environment:
106
-
107
- ```bash
108
- npx @wpmoo/toolkit
109
- ```
110
-
111
- The cockpit starts with a fast environment status summary, then opens a compact menu designed for repeated local work:
112
-
113
- ```text
114
- Command palette /
115
- Services
116
- Modules
117
- Database
118
- Diagnostics
119
- Repositories
120
- Maintenance
121
- Exit
122
- ```
123
-
124
- The UI is intentionally practical rather than decorative:
125
-
126
- - `Command palette /` searches slash commands such as `/test`, `/logs`, `/doctor`, and `/safe-reset`.
127
- - Category menus group related tasks for scanability: services, modules, database, diagnostics, repositories, and maintenance.
128
- - `Esc` returns from category menus to the top-level cockpit.
129
- - Empty states explain the next action, such as adding a source repo before selecting a module.
130
- - Risky commands such as stopping services, resetting databases, restoring snapshots, removing repos, removing modules, and safe reset ask for explicit confirmation.
131
- - Guided prompts collect common arguments for daily actions, including module names, database names, test modes, tags, snapshot names, and POT output paths.
132
-
133
- ## Cockpit Command Map
134
-
135
- | Category | Commands |
136
- | --- | --- |
137
- | Services | `start`, `stop`, `restart`, `logs`, `shell` |
138
- | Modules | `install`, `update`, `test`, `lint`, `pot`, `add-module`, `remove-module` |
139
- | Database | `psql`, `snapshot`, `restore-snapshot`, `resetdb` |
140
- | Diagnostics | `status`, `doctor` |
141
- | Repositories | `add-repo`, `remove-repo` |
142
- | Maintenance | `safe-reset` |
143
-
144
- Every cockpit action maps to a direct command, or to an equivalent management command such as `/safe-reset` mapping to `reset`, for scripting and repeatable terminal workflows.
145
-
146
- ## Direct Commands
147
-
148
- ```bash
149
- npx @wpmoo/toolkit --help
150
- npx @wpmoo/toolkit --version
151
-
152
- npx @wpmoo/toolkit status
153
- npx @wpmoo/toolkit status --json
154
- npx @wpmoo/toolkit doctor
155
- npx @wpmoo/toolkit doctor --json
156
- npx @wpmoo/toolkit doctor --fix
157
- npx @wpmoo/toolkit source list --json
158
- npx @wpmoo/toolkit add-repo --repo-url https://github.com/example-org/odoo_sample_module_reports.git
159
- npx @wpmoo/toolkit remove-repo --repo odoo_sample_module_reports
160
- npx @wpmoo/toolkit add-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
161
- npx @wpmoo/toolkit remove-module --repo odoo_sample_module --module odoo_sample_module_base --source-type private
162
- npx @wpmoo/toolkit reset --dry-run
163
- npx @wpmoo/toolkit reset
164
-
165
- npx @wpmoo/toolkit start
166
- npx @wpmoo/toolkit stop
167
- npx @wpmoo/toolkit restart
168
- npx @wpmoo/toolkit logs odoo
169
- npx @wpmoo/toolkit shell
170
- npx @wpmoo/toolkit psql postgres
171
-
172
- npx @wpmoo/toolkit install sale devel
173
- npx @wpmoo/toolkit update sale devel
174
- npx @wpmoo/toolkit test sale --db devel --mode update --tags /sale
175
- npx @wpmoo/toolkit lint
176
- npx @wpmoo/toolkit pot sale devel i18n/sale.pot
177
-
178
- npx @wpmoo/toolkit resetdb devel sale
179
- npx @wpmoo/toolkit snapshot devel before-update
180
- npx @wpmoo/toolkit restore-snapshot --dry-run before-update devel
181
- npx @wpmoo/toolkit restore-snapshot before-update devel
90
+ --init-empty-repos
182
91
  ```
183
92
 
184
- Daily action commands must be run from a generated environment root containing `.wpmoo/odoo.json`. They delegate to fixed scripts under `./scripts`; they do not search parent directories or run arbitrary script names.
93
+ ## Main Cockpit Menu
185
94
 
186
- ## Generated Environment Layout
187
-
188
- A generated environment is a separate Git repository, usually named `<product>_dev`, but the wizard and `--target` can use any folder. Product source code stays in child source repositories.
95
+ The cockpit is the daily workspace. It starts with environment status and then shows a compact menu:
189
96
 
190
97
  ```text
191
- odoo_sample_module_dev/
192
- |-- .wpmoo/
193
- | `-- odoo.json
194
- |-- .env.example
195
- |-- AGENTS.md
196
- |-- README.md
197
- |-- compose.yaml
198
- |-- compose/
199
- | |-- dev.yaml
200
- | |-- stage.yaml
201
- | `-- prod.yaml
202
- |-- config/
203
- | `-- odoo/
204
- | `-- odoo.conf
205
- |-- docs/
206
- | |-- appstore-release.md
207
- | `-- compose.md
208
- |-- resources/
209
- | `-- odoo/
210
- | `-- entrypoint.sh
211
- |-- moo
212
- |-- odoo/
213
- | `-- custom/
214
- | `-- src/
215
- | |-- private/
216
- | |-- oca/
217
- | `-- external/
218
- `-- scripts/
219
- ```
220
-
221
- Development uses `compose.yaml` plus `compose/dev.yaml` by default. Set
222
- `WPMOO_ENV=stage` or `WPMOO_ENV=prod` only after providing production-grade
223
- secrets and volumes.
224
-
225
- The metadata file `.wpmoo/odoo.json` records the product slug, selected Odoo version, dev repo URL, source repos, engine, external resource refs, ports, and template configuration. Status, doctor, daily actions, and safe reset use that metadata instead of guessing from the filesystem.
226
-
227
- ## Daily `./moo` Commands
228
-
229
- Generated environments include a local `./moo` dispatcher. It is the shortest path for everyday Compose and Odoo work:
98
+ WPMoo Cockpit
99
+ |-- Command palette /
100
+ | |-- search commands such as /test, /logs, /doctor, /safe-reset
101
+ |-- Services
102
+ | |-- start
103
+ | |-- stop
104
+ | |-- restart
105
+ | |-- logs
106
+ | `-- shell
107
+ |-- Modules
108
+ | |-- install
109
+ | |-- update
110
+ | |-- test
111
+ | |-- lint
112
+ | |-- pot
113
+ | |-- add-module
114
+ | `-- remove-module
115
+ |-- Database
116
+ | |-- psql
117
+ | |-- snapshot
118
+ | |-- restore-snapshot
119
+ | `-- resetdb
120
+ |-- Diagnostics
121
+ | |-- status
122
+ | `-- doctor
123
+ |-- Repositories
124
+ | |-- add-repo
125
+ | `-- remove-repo
126
+ |-- Maintenance
127
+ | `-- safe-reset
128
+ `-- Exit
129
+ ```
130
+
131
+ Every cockpit action maps to a direct command, so the same workflow can be used interactively or scripted:
230
132
 
231
133
  ```bash
232
- cp .env.example .env
233
-
234
134
  ./moo start
235
135
  ./moo logs odoo
236
- ./moo shell
237
- ./moo psql postgres
238
- ./moo restart
239
- ./moo stop
240
-
241
- ./moo install sale devel
242
- ./moo update sale devel
243
- ./moo test sale --db devel --mode update --tags /sale
244
- ./moo lint
245
- ./moo pot sale devel i18n/sale.pot
246
-
136
+ ./moo update sale
137
+ ./moo test sale
247
138
  ./moo snapshot devel before-update
248
139
  ./moo restore-snapshot --dry-run before-update devel
249
- ./moo restore-snapshot before-update devel
250
- ./moo resetdb devel sale
251
- ```
252
-
253
- `restore-snapshot --dry-run` validates the selected snapshot and prints the
254
- restore plan without changing the database or filestore. Generated environments
255
- also support `WPMOO_SNAPSHOT_RETENTION_COUNT` for pruning old snapshot files.
256
- When `WPMOO_ENV=stage` or `WPMOO_ENV=prod`, destructive database actions such
257
- as `resetdb` and real `restore-snapshot` require `WPMOO_ALLOW_DESTRUCTIVE=1`.
258
-
259
- Use `npx @wpmoo/toolkit ...` for package/operator commands such as `create`, `add-repo`, `remove-repo`, `add-module`, `remove-module`, `status`, `doctor`, and `reset`. Use `./moo ...` inside a generated environment for local daily Compose commands.
260
-
261
- ## Repository and Module Management
262
-
263
- Add a source repository after local-only setup from the cockpit or direct command:
264
-
265
- ```bash
266
- npx @wpmoo/toolkit add-repo \
267
- --repo-url https://github.com/example-org/odoo_sample_module_reports.git \
268
- --init-empty-repos
269
- ```
270
-
271
- Pin source repositories to dedicated source directories:
272
-
273
- ```bash
274
- npx @wpmoo/toolkit add-repo \
275
- --repo-url https://github.com/OCA/sale-workflow.git \
276
- --source-type oca
277
-
278
- npx @wpmoo/toolkit add-repo \
279
- --repo-url https://github.com/example-org/odoo_external_tool.git \
280
- --source-type external
281
- ```
282
-
283
- GitHub CLI is optional for repository setup. When it is available and authenticated, the interactive flow can:
284
-
285
- - detect the owner or organization from the current environment;
286
- - suggest repository URLs;
287
- - check whether the repository is accessible;
288
- - create inaccessible repositories after confirmation;
289
- - initialize empty repositories with the selected Odoo branch.
290
-
291
- Add a minimal Odoo module skeleton to a source repository:
292
-
293
- For module actions, `--source-type` selects the source directory (`private`, `oca`, or `external`). Default is `private`.
294
-
295
- ```bash
296
- npx @wpmoo/toolkit add-module \
297
- --repo odoo_sample_module \
298
- --module odoo_sample_module_base \
299
- --source-type oca
300
- ```
301
-
302
- Remove a module registration while keeping files:
303
-
304
- ```bash
305
- npx @wpmoo/toolkit remove-module \
306
- --repo odoo_sample_module \
307
- --module odoo_sample_module_base \
308
- --source-type oca
309
140
  ```
310
141
 
311
- Delete module files as well:
142
+ Module source actions also have direct commands. Default is `private`; pass `--source-type oca` or `--source-type external` for non-private source repositories:
312
143
 
313
144
  ```bash
314
- npx @wpmoo/toolkit remove-module \
315
- --repo odoo_sample_module \
316
- --module odoo_sample_module_base \
317
- --delete-files
145
+ npx @wpmoo/toolkit add-module --repo sale-workflow --module sale_order_line_no_discount --source-type oca
146
+ npx @wpmoo/toolkit remove-module --repo sale-workflow --module sale_order_line_no_discount --source-type oca
318
147
  ```
319
148
 
320
- Remove a source repository submodule:
321
-
322
- ```bash
323
- npx @wpmoo/toolkit remove-repo --repo odoo_sample_module_reports
324
- ```
325
-
326
- WPMoo refuses to remove a source repo submodule when that submodule has uncommitted changes.
327
-
328
- Generated environments also keep a deterministic source manifest at
329
- `odoo/custom/manifests/sources.yaml`. It mirrors source submodules from
330
- `.wpmoo/odoo.json` and `.gitmodules`, including source type, path, URL, branch,
331
- and addon boundaries.
332
-
333
- Inspect configured sources:
334
-
335
- ```bash
336
- npx @wpmoo/toolkit source list
337
- npx @wpmoo/toolkit source list --json
338
- ```
339
-
340
- Regenerate the manifest and metadata from the current metadata/gitmodule state:
341
-
342
- ```bash
343
- npx @wpmoo/toolkit source sync
344
- npx @wpmoo/toolkit source sync --json
345
- ```
346
-
347
- `source add` and `source remove` are direct aliases for the same repository
348
- operations:
349
-
350
- ```bash
351
- npx @wpmoo/toolkit source add \
352
- --repo-url https://github.com/OCA/server-tools.git \
353
- --source-type oca
354
-
355
- npx @wpmoo/toolkit source remove --repo server-tools --source-type oca
356
- ```
357
-
358
- ## Status, Doctor, and Recovery
359
-
360
- `status` is fast and offline. It reads local metadata and files only:
361
-
362
- ```bash
363
- npx @wpmoo/toolkit status
364
- npx @wpmoo/toolkit status --json
365
- ```
366
-
367
- It reports whether the environment is detected, which Odoo version is selected, how many source repos are configured, how many module candidates are present, which core files are missing, and the recommended next action.
368
-
369
- For automation and VS Code cockpit integration, all of these commands also support
370
- `--json`:
149
+ For automation and VS Code cockpit integration, selected commands support JSON output:
371
150
 
372
151
  ```bash
373
152
  npx @wpmoo/toolkit status --json
@@ -378,143 +157,16 @@ npx @wpmoo/toolkit doctor --json
378
157
 
379
158
  JSON output is optional; human-readable output remains the default.
380
159
 
381
- `doctor` performs deeper checks:
382
-
383
- ```bash
384
- npx @wpmoo/toolkit doctor
385
- ```
386
-
387
- It validates metadata, engine support, selected compose files, source repo paths,
388
- source manifest consistency, daily scripts, `.env` settings, Docker CLI access,
389
- Docker Compose access, GitHub CLI authentication when available, and PostgreSQL
390
- 18 compatibility in compose mount targets (for mounts to
391
- `/var/lib/postgresql/data` or `/var/lib/postgresql/18/docker`).
392
-
393
- Use `doctor --fix` for safe file-level repairs. It can normalize PostgreSQL 18
394
- mount targets and regenerate `odoo/custom/manifests/sources.yaml` from
395
- metadata plus `.gitmodules`, then it runs doctor again and reports any remaining
396
- manual issues.
397
-
398
- Safe reset refreshes generated environment files without deleting product source code:
399
-
400
- ```bash
401
- npx @wpmoo/toolkit reset --dry-run
402
- npx @wpmoo/toolkit reset
403
- ```
404
-
405
- Safe reset updates generated files such as `.wpmoo/odoo.json`, `moo`,
406
- `.gitignore`, `.env.example`, generated docs, compose assets, and optional
407
- Agent Skills. Compose overlays like `compose.yaml` and `compose/dev.yaml` are
408
- also refreshed from the current compose template source.
409
-
410
- Use `reset --dry-run` first when you want a deterministic preview of refreshed
411
- files and cleanup warnings without writing to the environment.
412
-
413
- It does not touch source repo folders under
414
- `odoo/custom/src/private`, module source code, Git history, remotes, or
415
- branches. It also preserves local runtime artifacts and custom source layout
416
- content:
417
-
418
- - `.env`, `data`, and `backups`
419
- - `odoo/custom/src/oca`, `odoo/custom/src/external`, `odoo/custom/patches`,
420
- `odoo/custom/manifests`, and their existing contents
421
-
422
- Legacy compose template paths from older scaffolds can remain
423
- (`docs/assets/`, `test/`, `.github/`) until you remove them manually.
160
+ ## Documentation
424
161
 
425
- Recommended recovery pattern:
426
-
427
- ```bash
428
- ./moo snapshot devel before-reset
429
- npx @wpmoo/toolkit reset --dry-run
430
- npx @wpmoo/toolkit reset
431
- npx @wpmoo/toolkit doctor --fix
432
- ./moo restore-snapshot --dry-run before-reset devel
433
- ./moo restore-snapshot before-reset devel
434
- ```
435
-
436
- ## External Resources
437
-
438
- WPMoo Toolkit keeps the package small by copying external resources into generated environments:
439
-
440
- ```text
441
- gh:wpmoo-org/odoo-docker-compose
442
- gh:wpmoo-org/odoo-skills
443
- ```
444
-
445
- Use the default resources:
446
-
447
- ```bash
448
- npx @wpmoo/toolkit create \
449
- --product odoo_sample_module \
450
- --source-repo-url https://github.com/example-org/odoo_sample_module.git \
451
- --agent-skills-template
452
- ```
453
-
454
- Pin external resource refs:
455
-
456
- ```bash
457
- npx @wpmoo/toolkit create \
458
- --product odoo_sample_module \
459
- --source-repo-url https://github.com/example-org/odoo_sample_module.git \
460
- --compose-template-ref v0.1.0 \
461
- --agent-skills-template \
462
- --agent-skills-template-ref v0.1.0
463
- ```
464
-
465
- Use local resource clones while developing the resource packages:
466
-
467
- ```bash
468
- git clone https://github.com/wpmoo-org/odoo-docker-compose ../odoo-docker-compose
469
- git clone https://github.com/wpmoo-org/odoo-skills ../odoo-skills
470
-
471
- npx @wpmoo/toolkit create \
472
- --engine compose \
473
- --compose-template-url ../odoo-docker-compose \
474
- --agent-skills-template \
475
- --agent-skills-template-url ../odoo-skills \
476
- --product odoo_sample_module \
477
- --source-repo-url https://github.com/example-org/odoo_sample_module.git
478
- ```
479
-
480
- More detail: [External Resources](docs/external-resources.md).
481
-
482
- ## Verification
483
-
484
- Run local package checks from the repository root:
485
-
486
- ```bash
487
- npm run typecheck
488
- npm test
489
- npm run test:coverage
490
- npm run build
491
- ```
492
-
493
- Generated environment behavior is covered by the operator-facing matrix in [Generated Environment Verification](docs/generated-environment-verification.md).
494
-
495
- ## Release
496
-
497
- The normal release path uses the repository helper and GitHub Actions trusted publishing:
498
-
499
- ```bash
500
- npm run release:check
501
- npm run typecheck
502
- npm test
503
- npm run build
504
- VERSION="$(node -p "require('./package.json').version")"
505
- git tag -a "v$VERSION" -m "Release v$VERSION"
506
- git push origin "v$VERSION"
507
- ```
162
+ - [External Resources](docs/external-resources.md)
163
+ - [Generated Environment Verification](docs/generated-environment-verification.md)
164
+ - Public documentation site: <https://wpmoo.org>
508
165
 
509
- If `npm run release:check` bumps `package.json` and `package-lock.json`, commit and push that version bump first, then rerun the release check before tagging. Publishing is handled by the `Publish` workflow after the tag is pushed.
166
+ ## License
510
167
 
511
- ## Sponsoring
168
+ WPMoo Toolkit is free software released under the [MIT License](LICENSE).
512
169
 
513
- Support ongoing WPMoo development through recurring or one-time sponsorship:
170
+ ## Acknowledgements
514
171
 
515
- <a href="https://www.buymeacoffee.com/cangir">
516
- <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" width="250">
517
- </a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
518
- <a href="https://www.patreon.com/wpmoo">
519
- <img src="docs/assets/patreon-donate.png" alt="Support WPMoo on Patreon" width="250">
520
- </a>
172
+ WPMoo builds on the work of many open source projects and communities. Thanks to the maintainers and contributors behind Odoo, OCA, Docker Compose, TypeScript, Node.js, Inquirer, Vitest, VitePress, GitHub CLI, npm, and the wider Odoo developer ecosystem.
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { confirmCockpitCommandRisk } from './cockpit/safety.js';
14
14
  import { detectDevelopmentEnvironment } from './environment.js';
15
15
  import { commandOdooVersion } from './environment-version.js';
16
16
  import { defaultAgentSkillsTemplateUrl } from './external-templates.js';
17
- import { listEnvironmentDatabases } from './databases.js';
17
+ import { listEnvironmentDatabases, normalizeDatabaseListResult } from './databases.js';
18
18
  import { isDailyActionCommand, runDailyAction, runDailyActionWithStyledOutput } from './daily-actions.js';
19
19
  import { getDoctorReport, runDoctor } from './doctor.js';
20
20
  import { getOriginUrl, realGit } from './git.js';
@@ -33,6 +33,7 @@ import { backupTargetPath, expectedTargetConfirmation, inspectEnvironmentTarget,
33
33
  import { getGitHubPrerequisiteStatus, renderGitHubPrerequisiteGuidance, } from './github-prerequisites.js';
34
34
  import { checkGitHubRepositories, createGitHubRepositories, manualCreateCommands, } from './repository-preflight.js';
35
35
  import { scaffold } from './scaffold.js';
36
+ import { getSystemPrerequisiteStatus, renderSystemPrerequisiteGuidance, } from './system-prerequisites.js';
36
37
  import { confirmPrompt, introPrompt, isPromptCancel, notePrompt, outroPrompt, selectPrompt, textPrompt } from './prompts/index.js';
37
38
  import { renderBanner } from './templates.js';
38
39
  import { checkForUpdate, isUpdateCheckSkipped, restartCli } from './update-check.js';
@@ -216,6 +217,11 @@ function clearCockpitScreen() {
216
217
  process.stdout.write('\u001B[2J\u001B[H');
217
218
  }
218
219
  }
220
+ function clearPrerequisiteScreen() {
221
+ if (process.stdout.isTTY) {
222
+ process.stdout.write('\u001B[3J\u001B[2J\u001B[H');
223
+ }
224
+ }
219
225
  const ANSI_ACTION = '\u001B[38;2;226;184;96m';
220
226
  const ANSI_SUCCESS = '\u001B[32m';
221
227
  const ANSI_DEFAULT_FOREGROUND = '\u001B[39m';
@@ -834,7 +840,7 @@ async function removeModuleOptionsFromPrompts(showIntro = true, cancelAction = '
834
840
  throw new Error('No Odoo modules found');
835
841
  }
836
842
  const deleteFiles = await confirmPrompt({
837
- message: menuPromptMessage('Delete module files too? (y/N)', cancelAction),
843
+ message: menuPromptMessage('Delete module files too?', cancelAction),
838
844
  active: 'Y',
839
845
  inactive: 'n',
840
846
  initialValue: false,
@@ -927,6 +933,40 @@ async function ensureGitHubRepositories(options, interactive) {
927
933
  handleCancel(visibility, 'exit');
928
934
  await createGitHubRepositories(missing, visibility);
929
935
  }
936
+ async function ensureSystemPrerequisites(interactive) {
937
+ while (true) {
938
+ const status = await getSystemPrerequisiteStatus();
939
+ if (status.ok) {
940
+ return true;
941
+ }
942
+ const guidance = renderSystemPrerequisiteGuidance(status);
943
+ if (!interactive) {
944
+ throw new Error(guidance);
945
+ }
946
+ clearPrerequisiteScreen();
947
+ console.log(renderStartupBanner());
948
+ console.log(renderVersionTag());
949
+ console.log();
950
+ console.log(guidance);
951
+ console.log();
952
+ const action = await selectPrompt({
953
+ message: 'If you have installed the prerequisites',
954
+ options: [
955
+ {
956
+ value: 'check-again',
957
+ label: `${renderActionText('Check again')}${dim(' (Enter to re-check again)')}`,
958
+ },
959
+ ],
960
+ initialValue: 'check-again',
961
+ loop: false,
962
+ navigationHelp: 'exit',
963
+ });
964
+ handleCancel(action, 'exit');
965
+ if (action === 'check-again') {
966
+ continue;
967
+ }
968
+ }
969
+ }
930
970
  async function ensureNonInteractiveCreateTarget(options) {
931
971
  if (options.dryRun) {
932
972
  return;
@@ -943,7 +983,7 @@ async function ensureNonInteractiveCreateTarget(options) {
943
983
  }
944
984
  throw new Error(renderForeignEnvironmentTargetWarning(state));
945
985
  }
946
- async function finishCreateFlow(result, cwd, interactive) {
986
+ async function finishCreateFlow(result, cwd, interactive, checkSystemPrerequisites = true) {
947
987
  if (result.kind === 'cancelled') {
948
988
  outroPrompt('Create flow cancelled.');
949
989
  return;
@@ -957,6 +997,9 @@ async function finishCreateFlow(result, cwd, interactive) {
957
997
  return;
958
998
  }
959
999
  const { options } = result;
1000
+ if (!options.dryRun && checkSystemPrerequisites && !(await ensureSystemPrerequisites(interactive))) {
1001
+ return;
1002
+ }
960
1003
  await ensureGitHubRepositories(options, interactive);
961
1004
  const scaffoldResult = await scaffold(options);
962
1005
  if (options.dryRun) {
@@ -1014,7 +1057,7 @@ function moduleActionTitle(action) {
1014
1057
  if (action === 'test')
1015
1058
  return 'Test module';
1016
1059
  if (action === 'lint')
1017
- return 'Run lint';
1060
+ return 'Run environment lint';
1018
1061
  if (action === 'delete')
1019
1062
  return 'Delete module';
1020
1063
  return 'Module action';
@@ -1025,7 +1068,7 @@ function moduleActionCompletedLabel(action) {
1025
1068
  if (action === 'test')
1026
1069
  return 'Test';
1027
1070
  if (action === 'lint')
1028
- return 'Lint';
1071
+ return 'Environment lint';
1029
1072
  return 'Action';
1030
1073
  }
1031
1074
  function commandActionTitle(command) {
@@ -1034,7 +1077,7 @@ function commandActionTitle(command) {
1034
1077
  if (command === 'test')
1035
1078
  return 'Test module';
1036
1079
  if (command === 'lint')
1037
- return 'Run lint';
1080
+ return 'Run environment lint';
1038
1081
  if (command === 'pot')
1039
1082
  return 'Generate POT';
1040
1083
  return command;
@@ -1047,7 +1090,7 @@ function commandCompletedLabel(command) {
1047
1090
  if (command === 'test')
1048
1091
  return 'Test';
1049
1092
  if (command === 'lint')
1050
- return 'Lint';
1093
+ return 'Environment lint';
1051
1094
  if (command === 'pot')
1052
1095
  return 'Generate POT';
1053
1096
  return command;
@@ -1065,7 +1108,8 @@ function dailyActionSelectedLabel(command, argv) {
1065
1108
  return undefined;
1066
1109
  }
1067
1110
  async function selectDatabaseArg(cwd, message, fallback, options = {}) {
1068
- const databases = await listEnvironmentDatabases(cwd, options);
1111
+ const databaseResult = normalizeDatabaseListResult(await listEnvironmentDatabases(cwd, options));
1112
+ const databases = databaseResult.databases;
1069
1113
  if (databases.length > 0) {
1070
1114
  const selected = await selectPrompt({
1071
1115
  message: menuPromptMessage(message, 'back'),
@@ -1081,7 +1125,7 @@ async function selectDatabaseArg(cwd, message, fallback, options = {}) {
1081
1125
  }
1082
1126
  }
1083
1127
  return asString(await textPrompt({
1084
- message: menuPromptMessage(message, 'back'),
1128
+ message: menuPromptMessage(databaseResult.ok ? message : `${message} (database list unavailable; enter manually)`, 'back'),
1085
1129
  defaultValue: fallback,
1086
1130
  placeholder: fallback,
1087
1131
  }), fallback, 'back');
@@ -1169,12 +1213,12 @@ async function runSelectedModuleDailyAction(action, module, cwd) {
1169
1213
  if (!command) {
1170
1214
  return false;
1171
1215
  }
1172
- return runDailyActionResultPage(command, moduleDailyActionArgs(action, module), cwd, moduleActionTitle(action), module.moduleName, moduleActionCompletedLabel(action));
1216
+ return runDailyActionResultPage(command, moduleDailyActionArgs(action, module), cwd, moduleActionTitle(action), action === 'lint' ? undefined : module.moduleName, moduleActionCompletedLabel(action));
1173
1217
  }
1174
1218
  async function runSelectedModuleAction(action, module, cwd) {
1175
1219
  if (action === 'delete') {
1176
1220
  const deleteFiles = await confirmPrompt({
1177
- message: menuPromptMessage('Delete module files too? (y/N)', 'back'),
1221
+ message: menuPromptMessage('Delete module files too?', 'back'),
1178
1222
  active: 'Y',
1179
1223
  inactive: 'n',
1180
1224
  initialValue: false,
@@ -1379,7 +1423,10 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1379
1423
  const detection = await detectDevelopmentEnvironment(cwd);
1380
1424
  if (!detection.isEnvironment) {
1381
1425
  await showStartup(argv, skipUpdateCheck);
1382
- await finishCreateFlow(await optionsFromPrompts(), cwd, true);
1426
+ if (!(await ensureSystemPrerequisites(true))) {
1427
+ return;
1428
+ }
1429
+ await finishCreateFlow(await optionsFromPrompts(), cwd, true, false);
1383
1430
  return;
1384
1431
  }
1385
1432
  let lastStatus = 'Last: Ready';
@@ -1546,7 +1593,10 @@ export async function runCli(cliArgv = process.argv.slice(2), cwd = process.cwd(
1546
1593
  await finishCreateFlow({ kind: 'create', options }, cwd, false);
1547
1594
  return;
1548
1595
  }
1549
- await finishCreateFlow(await optionsFromPrompts(), cwd, true);
1596
+ if (!(await ensureSystemPrerequisites(true))) {
1597
+ return;
1598
+ }
1599
+ await finishCreateFlow(await optionsFromPrompts(), cwd, true, false);
1550
1600
  }
1551
1601
  export function isCliEntrypoint(metaUrl, argvPath = process.argv[1]) {
1552
1602
  if (!argvPath)
@@ -41,7 +41,7 @@ export const cockpitCommands = [
41
41
  dailyCommand('install', 'modules', 'Install module', 'Install one or more Odoo modules into a database.', ['install module']),
42
42
  dailyCommand('update', 'modules', 'Update module', 'Update one or more Odoo modules in a database.', ['upgrade']),
43
43
  dailyCommand('test', 'modules', 'Run tests', 'Run Odoo tests for one or more modules.', ['tests', 'pytest']),
44
- dailyCommand('lint', 'modules', 'Run lint', 'Run the configured module lint checks.', ['check', 'quality']),
44
+ dailyCommand('lint', 'modules', 'Run environment lint', 'Run the configured environment lint checks.', ['check', 'quality']),
45
45
  dailyCommand('pot', 'modules', 'Generate POT', 'Generate translation template files for a module.', ['translation', 'i18n']),
46
46
  dailyCommand('psql', 'database', 'Open psql', 'Open a PostgreSQL prompt for an environment database.', ['postgres', 'sql']),
47
47
  dailyCommand('snapshot', 'database', 'Create snapshot', 'Create a database snapshot.', ['backup', 'dump']),
@@ -1,4 +1,4 @@
1
- import { listEnvironmentDatabases } from '../databases.js';
1
+ import { listEnvironmentDatabases, normalizeDatabaseListResult, } from '../databases.js';
2
2
  import { listModulesInSourceRepo } from '../module-actions.js';
3
3
  import { listModuleRepos } from '../repo-actions.js';
4
4
  import { listSources } from '../source-actions.js';
@@ -84,7 +84,8 @@ async function optionalTextArg(deps, message, fallback) {
84
84
  }), fallback, deps);
85
85
  }
86
86
  async function databaseArg(cwd, deps, message, fallback, options = {}) {
87
- const databases = await deps.databases(cwd, options);
87
+ const databaseResult = normalizeDatabaseListResult(await deps.databases(cwd, options));
88
+ const databases = databaseResult.databases;
88
89
  if (databases.length > 0) {
89
90
  const selected = await deps.list({
90
91
  message: menuPromptMessage(message, 'back'),
@@ -99,7 +100,7 @@ async function databaseArg(cwd, deps, message, fallback, options = {}) {
99
100
  return String(selected);
100
101
  }
101
102
  }
102
- return optionalTextArg(deps, message, fallback);
103
+ return optionalTextArg(deps, databaseResult.ok ? message : `${message} (database list unavailable; enter manually)`, fallback);
103
104
  }
104
105
  async function optionalModules(cwd, deps) {
105
106
  const modules = await detectedModules(cwd);
@@ -4,7 +4,7 @@ const moduleActions = [
4
4
  { id: 'delete', label: 'Delete module' },
5
5
  { id: 'update', label: 'Update' },
6
6
  { id: 'test', label: 'Test' },
7
- { id: 'lint', label: 'Lint' },
7
+ { id: 'lint', label: 'Run environment lint' },
8
8
  ];
9
9
  function defaultCancelHandler(value, action) {
10
10
  handlePromptCancel(isPromptCancel(value), action);
package/dist/databases.js CHANGED
@@ -22,6 +22,12 @@ export function parseDatabaseListOutput(output, options = {}) {
22
22
  }
23
23
  return databases;
24
24
  }
25
+ export function normalizeDatabaseListResult(result) {
26
+ if (Array.isArray(result)) {
27
+ return { ok: true, databases: result };
28
+ }
29
+ return result;
30
+ }
25
31
  export async function listEnvironmentDatabases(cwd, options = {}) {
26
32
  const queryLiteral = JSON.stringify(listDatabasesQuery);
27
33
  const command = [
@@ -32,15 +38,21 @@ export async function listEnvironmentDatabases(cwd, options = {}) {
32
38
  return new Promise((resolve) => {
33
39
  const child = spawn('bash', ['-lc', command], {
34
40
  cwd,
35
- stdio: ['ignore', 'pipe', 'ignore'],
41
+ stdio: ['ignore', 'pipe', 'pipe'],
36
42
  });
37
43
  let output = '';
44
+ let errorOutput = '';
38
45
  child.stdout?.on('data', (chunk) => {
39
46
  output += chunk.toString('utf8');
40
47
  });
41
- child.on('error', () => resolve([]));
48
+ child.stderr?.on('data', (chunk) => {
49
+ errorOutput += chunk.toString('utf8');
50
+ });
51
+ child.on('error', (error) => resolve({ ok: false, databases: [], error: error.message }));
42
52
  child.on('close', (code) => {
43
- resolve(code === 0 ? parseDatabaseListOutput(output, options) : []);
53
+ resolve(code === 0
54
+ ? { ok: true, databases: parseDatabaseListOutput(output, options) }
55
+ : { ok: false, databases: [], error: errorOutput.trim() || `Database list command exited with ${code}` });
44
56
  });
45
57
  });
46
58
  }
package/dist/help.js CHANGED
@@ -91,6 +91,8 @@ Cockpit:
91
91
 
92
92
  Wizard local-only path:
93
93
  Run npx @wpmoo/toolkit from a workspace directory to open the create wizard.
94
+ Before setup starts, WPMoo checks Git, Docker, Docker Compose, and Docker Engine.
95
+ If required tools are missing, WPMoo offers installer guidance before writing files.
94
96
  Choose any environment folder; the default is ./<product>_dev.
95
97
  Skip Git/GitHub connection to create a local-only environment.
96
98
  Add source repos later from the cockpit or with add-repo.
@@ -129,22 +129,27 @@ function renderedNavigationWarning(navigationWarning) {
129
129
  const warning = typeof navigationWarning === 'function' ? navigationWarning() : navigationWarning;
130
130
  return warning ? `\u001B[2m\u001B[38;2;226;184;96m${warning}\u001B[0m` : undefined;
131
131
  }
132
- function hiddenSelectTheme(disabledError, navigationHelp = 'exit', navigationWarning) {
133
- const keysHelpTip = navigationHelp === 'back' ? '↑↓ navigate • ⏎ select • Esc to go back' : '↑↓ navigate • ⏎ select • Ctrl+C exit';
132
+ function hiddenSelectTheme(disabledError, navigationHelp = 'exit', navigationWarning, hideMessage = true) {
133
+ const keysHelpTip = navigationHelp === 'back'
134
+ ? '↑↓ navigate • ⏎ select • Esc to go back'
135
+ : '↑↓ navigate • ⏎ select • Ctrl+C exit';
136
+ const style = {
137
+ highlight: (text) => text,
138
+ disabled: (text) => styleText('dim', text.replace(/ \(disabled\)$/u, ''), { validateStream: false }),
139
+ keysHelpTip: () => {
140
+ const warning = renderedNavigationWarning(navigationWarning);
141
+ return warning ? `${warning}\n${keysHelpTip}` : keysHelpTip;
142
+ },
143
+ };
144
+ if (hideMessage) {
145
+ style.message = () => '';
146
+ }
134
147
  return {
135
148
  prefix: '',
136
149
  icon: {
137
150
  cursor: '\u001B[38;2;226;184;96m❯\u001B[39m',
138
151
  },
139
- style: {
140
- message: () => '',
141
- highlight: (text) => text,
142
- disabled: (text) => styleText('dim', text.replace(/ \(disabled\)$/u, ''), { validateStream: false }),
143
- keysHelpTip: () => {
144
- const warning = renderedNavigationWarning(navigationWarning);
145
- return warning ? `${warning}\n${keysHelpTip}` : keysHelpTip;
146
- },
147
- },
152
+ style,
148
153
  i18n: disabledError ? { disabledError } : undefined,
149
154
  };
150
155
  }
@@ -157,13 +162,10 @@ function withHiddenSelectMessage(config) {
157
162
  return config;
158
163
  }
159
164
  const { disabledError, hideMessage: _hideMessage, navigationHelp, navigationWarning, escapeBehavior: _escapeBehavior, ...inquirerConfig } = config;
160
- if (!config.hideMessage) {
161
- return inquirerConfig;
162
- }
163
165
  return {
164
166
  ...inquirerConfig,
165
- message: '',
166
- theme: hiddenSelectTheme(disabledError, navigationHelp, navigationWarning),
167
+ message: config.hideMessage ? '' : inquirerConfig.message,
168
+ theme: hiddenSelectTheme(disabledError, navigationHelp, navigationWarning, Boolean(config.hideMessage)),
167
169
  };
168
170
  }
169
171
  function asInquirerConfirmConfig(options) {
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/toolkit",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "WPMoo Toolkit for development, staging, and production lifecycle workflows.",
5
5
  "type": "module",
6
6
  "repository": {