@42wp/dev-env 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/42wp.js +10 -0
- package/package.json +42 -0
- package/src/cli.js +119 -0
- package/src/commands/global.js +62 -0
- package/src/commands/rm.js +82 -0
- package/src/commands/seed.js +42 -0
- package/src/commands/start.js +236 -0
- package/src/commands/stop.js +32 -0
- package/src/commands/update.js +78 -0
- package/src/commands/wp.js +28 -0
- package/src/lib/config.js +56 -0
- package/src/lib/docker.js +93 -0
- package/src/lib/i18n.js +64 -0
- package/src/lib/log.js +44 -0
- package/src/lib/naming.js +44 -0
- package/src/lib/paths.js +34 -0
- package/src/lib/prompt.js +17 -0
- package/src/lib/render.js +41 -0
- package/src/lib/salts.js +57 -0
- package/src/lib/seeder.js +14 -0
- package/src/lib/wait.js +26 -0
- package/src/locales/en.js +91 -0
- package/src/locales/pt.js +91 -0
- package/src/templates/demo-seed.php +202 -0
- package/src/templates/dev-helper.php +45 -0
- package/src/templates/docker-compose.global.yml +57 -0
- package/src/templates/htaccess.multisite-subdir +17 -0
- package/src/templates/htaccess.multisite-subdomain +17 -0
- package/src/templates/project.Dockerfile +16 -0
- package/src/templates/project.docker-compose.yml +29 -0
- package/src/templates/wp-config.php +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João Carvalho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# 42wp — 42WP dev environments
|
|
2
|
+
|
|
3
|
+
> *O Guia do Mochileiro do WordPress* — a tiny CLI that spins up disposable
|
|
4
|
+
> WordPress dev environments with Docker, the 42WP way.
|
|
5
|
+
|
|
6
|
+
`42wp` runs a shared **global layer** (Traefik reverse proxy + MySQL + phpMyAdmin +
|
|
7
|
+
Mailpit) and, on top of it, **per-project** WordPress containers reachable at
|
|
8
|
+
`http://<project>.localhost`. Your current working directory is mounted as
|
|
9
|
+
`wp-content`, so you develop your theme/plugin locally and WordPress runs in a
|
|
10
|
+
container with Xdebug, Memcached and WP-CLI preinstalled.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- [Docker](https://docs.docker.com/get-docker/) with the Compose v2 plugin (`docker compose`)
|
|
15
|
+
- Node.js **>= 18.17**
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @42wp/dev-env
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This installs the `42wp` command globally.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
42wp global start # start Traefik + MySQL + phpMyAdmin + Mailpit
|
|
29
|
+
42wp start <project> # build & start a project (run from your theme/plugin repo)
|
|
30
|
+
42wp update <project> # update an existing project to a newer WordPress image
|
|
31
|
+
42wp wp <project> <args> # run a WP-CLI command inside the project container
|
|
32
|
+
42wp stop <project> # stop a project's containers
|
|
33
|
+
42wp rm <project> # remove a site (container, image, database) — keeps your repo
|
|
34
|
+
42wp seed <project> [n] # generate demo content in a running project (default 200)
|
|
35
|
+
42wp global stop # tear the global layer down
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run `42wp` with no arguments for the full command list.
|
|
39
|
+
|
|
40
|
+
### WordPress version
|
|
41
|
+
|
|
42
|
+
By default projects are built on **`wordpress:php8.4-apache`** (newest WordPress,
|
|
43
|
+
PHP 8.4, Apache). Pick a different image tag per project with `--wp <tag>`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
42wp start my-theme --wp latest # newest WP, image's default PHP
|
|
47
|
+
42wp start legacy --wp 6.9-php8.5-apache # pin a specific WP + PHP
|
|
48
|
+
42wp update my-theme # rebuild on the default tag, run wp core update-db
|
|
49
|
+
42wp update my-theme --wp php8.5-apache # update to a specific image
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`update` rewrites the project's Dockerfile, re-pulls the base image, recreates the
|
|
53
|
+
container and runs `wp core update-db` to migrate the schema. Set a different
|
|
54
|
+
default for every project with the `FORTYTWO_WP_TAG` env var.
|
|
55
|
+
|
|
56
|
+
### Removing a site
|
|
57
|
+
|
|
58
|
+
`42wp rm <project>` tears a site down completely — its containers, the locally
|
|
59
|
+
built image, and its database — and deletes the generated project dir
|
|
60
|
+
(`~/.42wp/projects/<name>/`, including any cloned VIP mu-plugins). **Your
|
|
61
|
+
repository is never touched**: wp-content is a bind mount of your working
|
|
62
|
+
directory, which Docker leaves alone. It asks for confirmation; skip the prompt
|
|
63
|
+
with `--yes` (or `--force`):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
42wp rm my-theme # prompts before removing
|
|
67
|
+
42wp rm my-theme --yes # no prompt (for scripts/CI)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Demo content
|
|
71
|
+
|
|
72
|
+
Seed a project with a full demo dataset using `--demo-content` (defaults to 200
|
|
73
|
+
posts), or set a count with `--demo-content=<n>`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
42wp start my-site --demo-content # ~200 posts
|
|
77
|
+
42wp start my-site --demo-content=50 # 50 posts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It runs a PHP seeder (`wp eval-file`) — no plugin required (FakerPress has no CLI) —
|
|
81
|
+
that creates:
|
|
82
|
+
|
|
83
|
+
- ~10 `42wp_author` terms (Portuguese names), ~10 tags, ~10 categories
|
|
84
|
+
- a small pool of real images downloaded from [picsum.photos](https://picsum.photos/),
|
|
85
|
+
reused as featured + inline images
|
|
86
|
+
- posts with rich HTML content (h1/h2/h3, lists, blockquote, images, links), a
|
|
87
|
+
plain-text excerpt, varied **past** dates, and each linked to 1 category, 2–5
|
|
88
|
+
tags and 1–3 authors
|
|
89
|
+
|
|
90
|
+
Works on any project (not just `--vip`). It always creates the requested number of
|
|
91
|
+
posts (it doesn't skip when posts already exist), and a failure never aborts
|
|
92
|
+
`start`. The author terms use the `42wp_author` taxonomy, which the project
|
|
93
|
+
registers itself (e.g. the `42-framework` mu-plugin) — the seeder does not register
|
|
94
|
+
any taxonomy. If `42wp_author` isn't available, the author links are simply skipped.
|
|
95
|
+
|
|
96
|
+
To add demo content to an **already-running** project without recreating it, use
|
|
97
|
+
`seed`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
42wp seed my-site # 200 posts
|
|
101
|
+
42wp seed my-site 500 # 500 posts
|
|
102
|
+
42wp seed my-site --demo-content=50
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Admin credentials
|
|
106
|
+
|
|
107
|
+
The silent install creates an `admin` / `password` user by default. Override per
|
|
108
|
+
project on `start`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
42wp start my-theme --user joao --pass s3cr3t
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Multisite
|
|
115
|
+
|
|
116
|
+
Start a site as a WordPress network with `--multisite` (subdirectory) or add
|
|
117
|
+
`--subdomains` for a subdomain network:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
42wp start my-network --multisite # sites at my-network.localhost/site2
|
|
121
|
+
42wp start my-network --multisite --subdomains # sites at site2.my-network.localhost
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This installs WordPress, converts it to a network, writes the `MULTISITE` constants
|
|
125
|
+
into `wp-config.php`, and mounts a `.htaccess` with the matching network rewrite
|
|
126
|
+
rules (WP-CLI refuses to generate those for a multisite, which otherwise causes a
|
|
127
|
+
redirect loop on sub-sites). For **subdomain** networks the Traefik router is
|
|
128
|
+
widened to a wildcard (`*.my-network.localhost`) so sub-sites route to the
|
|
129
|
+
container; `*.localhost` already resolves to `127.0.0.1`, so no `/etc/hosts` edits
|
|
130
|
+
are needed. Network admin lives at `…/wp-admin/network/`.
|
|
131
|
+
|
|
132
|
+
### WordPress VIP (mu-plugins)
|
|
133
|
+
|
|
134
|
+
WPVIP sites expect the VIP Go mu-plugins under `wp-content/mu-plugins`. Pass
|
|
135
|
+
`--vip` to `start` and the tool clones
|
|
136
|
+
[`vip-go-mu-plugins-built`](https://github.com/Automattic/vip-go-mu-plugins-built)
|
|
137
|
+
(shallow) and mounts it into the container at `wp-content/mu-plugins`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
42wp start my-vip-site --vip
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The clone lives in the project's data dir (`~/.42wp/projects/<name>/mu-plugins`),
|
|
144
|
+
so it never touches your repo, and re-running `start` fast-forwards it. Regular
|
|
145
|
+
(non-VIP) projects are unaffected.
|
|
146
|
+
|
|
147
|
+
`--vip` also mounts a small **dev-helper mu-plugin** (`00-42wp-dev.php`) that loads
|
|
148
|
+
the WordPress admin media includes in admin/REST/CLI contexts. This fixes
|
|
149
|
+
"Call to undefined function `media_handle_sideload()`"-style errors from plugins
|
|
150
|
+
like FakerPress, which the VIP environment triggers by loading `file.php` early.
|
|
151
|
+
It's mounted from the data dir, so your repo and the cloned VIP suite stay clean.
|
|
152
|
+
|
|
153
|
+
### Example
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
cd ~/code/my-theme
|
|
157
|
+
42wp global start
|
|
158
|
+
42wp start my-theme
|
|
159
|
+
# → http://my-theme.localhost (admin / password at /wp-admin)
|
|
160
|
+
42wp wp my-theme plugin list
|
|
161
|
+
42wp stop my-theme
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Shared services once the global layer is up:
|
|
165
|
+
|
|
166
|
+
| Service | URL |
|
|
167
|
+
| ----------- | ---------------------------- |
|
|
168
|
+
| Traefik | http://localhost:8080 |
|
|
169
|
+
| phpMyAdmin | http://db.42wp.localhost |
|
|
170
|
+
| Mailpit | http://mail.42wp.localhost |
|
|
171
|
+
|
|
172
|
+
## How it works
|
|
173
|
+
|
|
174
|
+
State lives under `~/.42wp` (override with `FORTYTWO_HOME`):
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
~/.42wp/
|
|
178
|
+
docker-compose.global.yml # the global layer (created on first run)
|
|
179
|
+
mysql-data/ # persisted MySQL data
|
|
180
|
+
projects/<project>/ # generated wp-config.php, Dockerfile, docker-compose.yml
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`42wp start` generates an ephemeral `wp-config.php` (fetching real salts from
|
|
184
|
+
api.wordpress.org, with a local fallback when offline), a `Dockerfile`, and a
|
|
185
|
+
`docker-compose.yml`, then builds the container and runs a silent
|
|
186
|
+
`wp core install`. The DB name is the project name with hyphens turned into
|
|
187
|
+
underscores; a trailing `.suffix` (e.g. `.localhost`) is stripped.
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
| Env var | Default | Description |
|
|
192
|
+
| --------------- | -------------- | --------------------------------------------- |
|
|
193
|
+
| `FORTYTWO_HOME` | `~/.42wp` | Where the global compose, DB data and projects live |
|
|
194
|
+
| `FORTYTWO_WP_TAG` | `php8.4-apache` | Default WordPress image tag for new projects (overridden by `--wp`) |
|
|
195
|
+
| `FORTYTWO_LANG` | auto (`LANG`) | UI language: `en` or `pt` |
|
|
196
|
+
| `NO_COLOR` | unset | Disable colored output |
|
|
197
|
+
|
|
198
|
+
You can also pass `--lang en` / `--lang pt` before the command.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
package/bin/42wp.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { run } from '../src/cli.js';
|
|
4
|
+
import { error } from '../src/lib/log.js';
|
|
5
|
+
|
|
6
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
7
|
+
// Top-level safety net: anything that bubbles up here is unexpected.
|
|
8
|
+
error(err && err.message ? err.message : String(err));
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@42wp/dev-env",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "42WP — a CLI to spin up disposable WordPress dev environments with Docker (Traefik + MySQL + per-project containers).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"42wp": "bin/42wp.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.17"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"prepublishOnly": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/42wp/dev-env.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/42wp/dev-env/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/42wp/dev-env#readme",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"wordpress",
|
|
30
|
+
"docker",
|
|
31
|
+
"cli",
|
|
32
|
+
"dev-environment",
|
|
33
|
+
"traefik",
|
|
34
|
+
"wp-cli",
|
|
35
|
+
"42wp"
|
|
36
|
+
],
|
|
37
|
+
"author": "João Carvalho <joao@carvalho.cc>",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Argument parsing and command dispatch — mirrors the `case "$1"` block in 42script.sh.
|
|
2
|
+
// A leading --lang / --lang=<locale> overrides the UI language; it's consumed before
|
|
3
|
+
// dispatch so it never leaks into WP-CLI arguments passed through `42wp wp`.
|
|
4
|
+
|
|
5
|
+
import { setLocale, t } from './lib/i18n.js';
|
|
6
|
+
import { plain, error } from './lib/log.js';
|
|
7
|
+
import { start } from './commands/start.js';
|
|
8
|
+
import { stop } from './commands/stop.js';
|
|
9
|
+
import { wp } from './commands/wp.js';
|
|
10
|
+
import { update } from './commands/update.js';
|
|
11
|
+
import { rm } from './commands/rm.js';
|
|
12
|
+
import { seed } from './commands/seed.js';
|
|
13
|
+
import { startGlobal, stopGlobal } from './commands/global.js';
|
|
14
|
+
|
|
15
|
+
function extractLang(argv) {
|
|
16
|
+
let lang;
|
|
17
|
+
while (argv.length && argv[0].startsWith('--lang')) {
|
|
18
|
+
const flag = argv.shift();
|
|
19
|
+
if (flag.includes('=')) lang = flag.slice(flag.indexOf('=') + 1);
|
|
20
|
+
else lang = argv.shift(); // value is the next token
|
|
21
|
+
}
|
|
22
|
+
return lang;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Flags that take a value (--wp tag, --user bob, --pass secret). Anything else
|
|
26
|
+
// (e.g. --vip) is a boolean. The `--key=value` form works for all of them.
|
|
27
|
+
const VALUE_FLAGS = new Set(['wp', 'user', 'pass']);
|
|
28
|
+
|
|
29
|
+
// Pull options out of an arg list, leaving positionals behind. Used by `start`
|
|
30
|
+
// and `update`; NOT by `wp`, whose args pass through verbatim to WP-CLI.
|
|
31
|
+
export function parseOpts(args) {
|
|
32
|
+
const opts = {};
|
|
33
|
+
const positionals = [];
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const a = args[i];
|
|
36
|
+
if (a.startsWith('--')) {
|
|
37
|
+
const eq = a.indexOf('=');
|
|
38
|
+
if (eq !== -1) {
|
|
39
|
+
opts[a.slice(2, eq)] = a.slice(eq + 1);
|
|
40
|
+
} else {
|
|
41
|
+
const key = a.slice(2);
|
|
42
|
+
if (VALUE_FLAGS.has(key)) opts[key] = args[++i];
|
|
43
|
+
else opts[key] = true; // boolean flag
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
positionals.push(a);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { opts, positionals };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function printUsage() {
|
|
53
|
+
plain(t('usage.line'));
|
|
54
|
+
plain('');
|
|
55
|
+
plain(t('usage.commands'));
|
|
56
|
+
plain(t('usage.start'));
|
|
57
|
+
plain(t('usage.update'));
|
|
58
|
+
plain(t('usage.stop'));
|
|
59
|
+
plain(t('usage.rm'));
|
|
60
|
+
plain(t('usage.seed'));
|
|
61
|
+
plain(t('usage.wp'));
|
|
62
|
+
plain(t('usage.globalStart'));
|
|
63
|
+
plain(t('usage.globalStop'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function run(rawArgs) {
|
|
67
|
+
const argv = [...rawArgs];
|
|
68
|
+
const lang = extractLang(argv);
|
|
69
|
+
setLocale(lang);
|
|
70
|
+
|
|
71
|
+
const command = argv[0];
|
|
72
|
+
|
|
73
|
+
switch (command) {
|
|
74
|
+
case 'start': {
|
|
75
|
+
const { opts, positionals } = parseOpts(argv.slice(1));
|
|
76
|
+
await start(positionals[0], opts);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'update': {
|
|
81
|
+
const { opts, positionals } = parseOpts(argv.slice(1));
|
|
82
|
+
await update(positionals[0], opts);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'stop':
|
|
87
|
+
await stop(argv[1]);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'rm': {
|
|
91
|
+
const { opts, positionals } = parseOpts(argv.slice(1));
|
|
92
|
+
await rm(positionals[0], opts);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'seed': {
|
|
97
|
+
const { opts, positionals } = parseOpts(argv.slice(1));
|
|
98
|
+
await seed(positionals[0], opts, positionals[1]);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'wp':
|
|
103
|
+
await wp(argv[1], argv.slice(2));
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'global':
|
|
107
|
+
if (argv[1] === 'start') await startGlobal();
|
|
108
|
+
else if (argv[1] === 'stop') await stopGlobal();
|
|
109
|
+
else {
|
|
110
|
+
error(t('global.invalid'));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
printUsage();
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `42wp global start|stop` and the shared global-layer bootstrap used by `start`.
|
|
2
|
+
// The global layer is Traefik + MySQL + phpMyAdmin + Mailpit on the 42wp_network.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
dockerIsRunning,
|
|
6
|
+
composeCapture,
|
|
7
|
+
compose,
|
|
8
|
+
dockerExecCapture,
|
|
9
|
+
} from '../lib/docker.js';
|
|
10
|
+
import { ensureGlobalCompose } from '../lib/render.js';
|
|
11
|
+
import { globalComposePath } from '../lib/paths.js';
|
|
12
|
+
import { pollUntil } from '../lib/wait.js';
|
|
13
|
+
import { step, error } from '../lib/log.js';
|
|
14
|
+
import { t } from '../lib/i18n.js';
|
|
15
|
+
|
|
16
|
+
// Exit early with a clear message if the Docker daemon isn't reachable.
|
|
17
|
+
export async function ensureDockerRunning() {
|
|
18
|
+
if (!(await dockerIsRunning())) {
|
|
19
|
+
error(t('docker.notRunning'));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function mysqlReady() {
|
|
27
|
+
const { code } = await dockerExecCapture('42wp_mysql', [
|
|
28
|
+
'mysqladmin',
|
|
29
|
+
'ping',
|
|
30
|
+
'-uroot',
|
|
31
|
+
'-proot',
|
|
32
|
+
'--silent',
|
|
33
|
+
]);
|
|
34
|
+
return code === 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Bring the global layer up if it isn't already running, then wait for MySQL.
|
|
38
|
+
// Replaces the bash `check_global_layer` (which used a fixed `sleep 10`).
|
|
39
|
+
export async function checkGlobalLayer() {
|
|
40
|
+
const file = await ensureGlobalCompose();
|
|
41
|
+
const { stdout } = await composeCapture(['-f', file, 'ps', '-q']);
|
|
42
|
+
if (stdout.length === 0) {
|
|
43
|
+
step(t('global.starting'));
|
|
44
|
+
await compose(['-f', file, 'up', '-d']);
|
|
45
|
+
step(t('global.waitingMysql'));
|
|
46
|
+
await pollUntil(mysqlReady, { label: 'MySQL', timeout: 90000 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function startGlobal() {
|
|
51
|
+
if (!(await ensureDockerRunning())) return;
|
|
52
|
+
const file = await ensureGlobalCompose();
|
|
53
|
+
step(t('global.starting'));
|
|
54
|
+
await compose(['-f', file, 'up', '-d']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function stopGlobal() {
|
|
58
|
+
if (!(await ensureDockerRunning())) return;
|
|
59
|
+
const file = globalComposePath();
|
|
60
|
+
step(t('global.stopping'));
|
|
61
|
+
await compose(['-f', file, 'down']);
|
|
62
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `42wp rm <project> [--yes]` — remove a site from the dev env.
|
|
2
|
+
//
|
|
3
|
+
// Removes the project's containers + locally-built image, drops its database, and
|
|
4
|
+
// deletes the generated project dir (~/.42wp/projects/<name>/, including any cloned
|
|
5
|
+
// VIP mu-plugins). It does NOT touch your repository: wp-content is a bind mount of
|
|
6
|
+
// your working directory, which `docker compose down` leaves untouched.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { normalizeName, validateName, dbName } from '../lib/naming.js';
|
|
12
|
+
import { projectDir } from '../lib/paths.js';
|
|
13
|
+
import { compose, dockerExec, containerIsRunning } from '../lib/docker.js';
|
|
14
|
+
import { confirm } from '../lib/prompt.js';
|
|
15
|
+
import { step, success, plain, error } from '../lib/log.js';
|
|
16
|
+
import { t } from '../lib/i18n.js';
|
|
17
|
+
import { ensureDockerRunning } from './global.js';
|
|
18
|
+
|
|
19
|
+
export async function rm(rawName, opts = {}) {
|
|
20
|
+
if (!rawName) {
|
|
21
|
+
error(t('rm.needName'));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const name = normalizeName(rawName);
|
|
27
|
+
validateName(name);
|
|
28
|
+
const db = dbName(name);
|
|
29
|
+
const envDir = projectDir(name);
|
|
30
|
+
|
|
31
|
+
// Must be a known project.
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(envDir);
|
|
34
|
+
} catch {
|
|
35
|
+
error(t('rm.notFound', { name, dir: envDir }));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Confirm unless --yes/--force. In a non-interactive shell we refuse rather
|
|
41
|
+
// than guess.
|
|
42
|
+
const skip = opts.yes || opts.force;
|
|
43
|
+
if (!skip) {
|
|
44
|
+
if (!process.stdin.isTTY) {
|
|
45
|
+
error(t('rm.needYes'));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const ok = await confirm(t('rm.confirm', { name }));
|
|
50
|
+
if (!ok) {
|
|
51
|
+
plain(t('rm.cancelled'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!(await ensureDockerRunning())) return;
|
|
57
|
+
|
|
58
|
+
// 1. Stop & remove containers + the image built for this project.
|
|
59
|
+
step(t('rm.removingContainers', { name }));
|
|
60
|
+
await compose(['down', '--rmi', 'local', '--remove-orphans'], { cwd: envDir });
|
|
61
|
+
|
|
62
|
+
// 2. Drop the database (only possible while the global MySQL is up).
|
|
63
|
+
if (await containerIsRunning('42wp_mysql')) {
|
|
64
|
+
step(t('rm.droppingDb', { db }));
|
|
65
|
+
await dockerExec('42wp_mysql', [
|
|
66
|
+
'mysql',
|
|
67
|
+
'-uroot',
|
|
68
|
+
'-proot',
|
|
69
|
+
'-e',
|
|
70
|
+
`DROP DATABASE IF EXISTS \`${db}\`;`,
|
|
71
|
+
]);
|
|
72
|
+
} else {
|
|
73
|
+
step(t('rm.mysqlDown', { db }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Delete the generated project dir (config, Dockerfile, compose, cloned
|
|
77
|
+
// mu-plugins). The repo bind-mounted as wp-content is never inside here.
|
|
78
|
+
step(t('rm.removingData', { dir: envDir }));
|
|
79
|
+
await fs.rm(envDir, { recursive: true, force: true });
|
|
80
|
+
|
|
81
|
+
success(t('rm.done', { name }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `42wp seed <project> [<count>] [--demo-content=<n>]` — generate demo content in
|
|
2
|
+
// an already-running project, without recreating the container. Always creates the
|
|
3
|
+
// requested number of posts (default 200).
|
|
4
|
+
|
|
5
|
+
import { normalizeName, validateName, dbName, containerName } from '../lib/naming.js';
|
|
6
|
+
import { containerIsRunning } from '../lib/docker.js';
|
|
7
|
+
import { runSeeder } from '../lib/seeder.js';
|
|
8
|
+
import { resolveDemoCount, DEFAULT_DEMO_COUNT } from '../lib/config.js';
|
|
9
|
+
import { step, error } from '../lib/log.js';
|
|
10
|
+
import { t } from '../lib/i18n.js';
|
|
11
|
+
import { ensureDockerRunning } from './global.js';
|
|
12
|
+
|
|
13
|
+
export async function seed(rawName, opts = {}, countArg) {
|
|
14
|
+
if (!rawName) {
|
|
15
|
+
error(t('seed.needName'));
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = normalizeName(rawName);
|
|
21
|
+
validateName(name);
|
|
22
|
+
const container = containerName(dbName(name));
|
|
23
|
+
|
|
24
|
+
if (!(await ensureDockerRunning())) return;
|
|
25
|
+
|
|
26
|
+
if (!(await containerIsRunning(container))) {
|
|
27
|
+
error(t('seed.notRunning', { name }));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Count from a positional arg or --demo-content=<n>; default 200.
|
|
33
|
+
const count = resolveDemoCount(countArg ?? opts['demo-content']) || DEFAULT_DEMO_COUNT;
|
|
34
|
+
|
|
35
|
+
step(t('start.demoGenerating', { count }));
|
|
36
|
+
try {
|
|
37
|
+
await runSeeder(container, count);
|
|
38
|
+
} catch {
|
|
39
|
+
error(t('seed.failed'));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
}
|