plugship 1.0.1 → 1.0.3
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 +352 -98
- package/package.json +1 -1
- package/src/cli.js +7 -1
- package/src/commands/ignore.js +0 -1
- package/src/lib/constants.js +1 -8
- package/src/lib/deployer.js +82 -8
- package/src/lib/wordpress-api.js +45 -6
- package/src/lib/zipper.js +5 -0
package/README.md
CHANGED
|
@@ -1,173 +1,322 @@
|
|
|
1
1
|
# plugship
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Deploy WordPress plugins from your terminal to any WordPress site instantly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/plugship)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- A WordPress site with REST API enabled
|
|
9
|
-
- An Administrator account with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/)
|
|
8
|
+
A simple CLI tool to deploy local WordPress plugins to remote WordPress sites. No FTP, no cPanel — just `plugship deploy`.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
**[View on npm →](https://www.npmjs.com/package/plugship)**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- 🚀 **One-command deploys** — `plugship deploy` and you're done
|
|
17
|
+
- 🔐 **Secure** — uses WordPress Application Passwords (not your main password)
|
|
18
|
+
- 🎯 **Multi-site** — configure once, deploy to staging/production/any site
|
|
19
|
+
- 📦 **Smart packaging** — auto-excludes dev files (node_modules, tests, src, etc.)
|
|
20
|
+
- 🔄 **Auto-updates** — replaces existing plugins automatically
|
|
21
|
+
- 🌐 **No server access needed** — works entirely via WordPress REST API
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
### Global Install (Recommended)
|
|
12
28
|
|
|
13
29
|
```bash
|
|
14
30
|
npm install -g plugship
|
|
15
31
|
```
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
### Verify Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
plugship --version
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🚀 Quick Start
|
|
42
|
+
|
|
43
|
+
### Step 1: Install the Receiver Plugin
|
|
18
44
|
|
|
19
|
-
|
|
45
|
+
The `plugship-receiver` plugin must be installed on your WordPress site first. It adds secure REST endpoints to receive plugin uploads.
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
**[Download plugship-receiver.zip →](https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip)**
|
|
22
48
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
49
|
+
1. Go to **Plugins > Add New > Upload Plugin** in WordPress admin
|
|
50
|
+
2. Upload `plugship-receiver.zip`
|
|
51
|
+
3. Activate **PlugShip Receiver**
|
|
26
52
|
|
|
27
|
-
|
|
53
|
+
**[View receiver plugin source →](https://github.com/shamim0902/plugship-receiver)**
|
|
54
|
+
|
|
55
|
+
### Step 2: Create an Application Password
|
|
28
56
|
|
|
29
57
|
1. Go to **Users > Profile** in WordPress admin
|
|
30
58
|
2. Scroll to **Application Passwords**
|
|
31
|
-
3. Enter
|
|
32
|
-
4.
|
|
59
|
+
3. Enter "plugship" as the name
|
|
60
|
+
4. Click **Add New Application Password**
|
|
61
|
+
5. Copy the generated password (you'll need it in the next step)
|
|
62
|
+
|
|
63
|
+
**[Learn more about Application Passwords →](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/)**
|
|
33
64
|
|
|
34
|
-
### 3
|
|
65
|
+
### Step 3: Configure Your First Site
|
|
35
66
|
|
|
36
67
|
```bash
|
|
37
68
|
plugship init
|
|
38
69
|
```
|
|
39
70
|
|
|
40
|
-
You
|
|
71
|
+
You'll be prompted for:
|
|
72
|
+
- **Site alias** — a short name like "production" or "staging"
|
|
73
|
+
- **WordPress site URL** — e.g., `https://example.com`
|
|
74
|
+
- **Admin username** — your WordPress admin username
|
|
75
|
+
- **Application Password** — paste the password from Step 2
|
|
76
|
+
|
|
77
|
+
### Step 4: Deploy Your Plugin
|
|
78
|
+
|
|
79
|
+
Navigate to your WordPress plugin directory:
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
```bash
|
|
82
|
+
cd my-awesome-plugin/
|
|
83
|
+
plugship deploy
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
✅ Done! Your plugin is deployed and activated on the remote site.
|
|
46
87
|
|
|
47
|
-
|
|
88
|
+
---
|
|
48
89
|
|
|
49
|
-
##
|
|
90
|
+
## 📚 Commands
|
|
50
91
|
|
|
51
|
-
###
|
|
92
|
+
### `plugship deploy`
|
|
52
93
|
|
|
53
|
-
|
|
94
|
+
Deploy the plugin from the current directory.
|
|
54
95
|
|
|
55
96
|
```bash
|
|
97
|
+
# Deploy to default site
|
|
56
98
|
plugship deploy
|
|
99
|
+
|
|
100
|
+
# Deploy to a specific site
|
|
101
|
+
plugship deploy --site staging
|
|
102
|
+
|
|
103
|
+
# Deploy to all configured sites
|
|
104
|
+
plugship deploy --all
|
|
105
|
+
|
|
106
|
+
# Preview what would be deployed (no upload)
|
|
107
|
+
plugship deploy --dry-run
|
|
108
|
+
|
|
109
|
+
# Deploy without activating the plugin
|
|
110
|
+
plugship deploy --no-activate
|
|
57
111
|
```
|
|
58
112
|
|
|
59
|
-
|
|
113
|
+
**What happens:**
|
|
114
|
+
1. Detects your plugin from PHP headers
|
|
115
|
+
2. Creates a ZIP excluding dev files
|
|
116
|
+
3. Uploads to the WordPress site
|
|
117
|
+
4. Installs/updates the plugin
|
|
118
|
+
5. Activates it (unless `--no-activate`)
|
|
60
119
|
|
|
61
|
-
|
|
62
|
-
2. Create a ZIP archive in the `build/` directory
|
|
63
|
-
3. Upload and install the plugin on the remote site
|
|
64
|
-
4. Activate the plugin
|
|
120
|
+
---
|
|
65
121
|
|
|
66
|
-
|
|
122
|
+
### `plugship init`
|
|
67
123
|
|
|
68
|
-
|
|
124
|
+
Add a new WordPress site to your configuration.
|
|
69
125
|
|
|
70
126
|
```bash
|
|
71
|
-
plugship
|
|
72
|
-
plugship deploy --no-activate # Deploy without activating the plugin
|
|
73
|
-
plugship deploy --dry-run # Preview what would be deployed without uploading
|
|
74
|
-
plugship deploy --all # Deploy to all configured sites
|
|
127
|
+
plugship init
|
|
75
128
|
```
|
|
76
129
|
|
|
77
|
-
|
|
130
|
+
Interactive prompts guide you through:
|
|
131
|
+
- Site alias
|
|
132
|
+
- URL
|
|
133
|
+
- Username
|
|
134
|
+
- Application Password
|
|
78
135
|
|
|
79
|
-
|
|
136
|
+
The CLI automatically tests the connection and verifies the receiver plugin is active.
|
|
80
137
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
plugship status
|
|
84
|
-
```
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `plugship status`
|
|
85
141
|
|
|
86
|
-
|
|
142
|
+
Check if a site is ready for deployment.
|
|
87
143
|
|
|
88
144
|
```bash
|
|
89
|
-
|
|
90
|
-
plugship
|
|
91
|
-
|
|
145
|
+
# Check default site
|
|
146
|
+
plugship status
|
|
147
|
+
|
|
148
|
+
# Check a specific site
|
|
149
|
+
plugship status --site staging
|
|
92
150
|
```
|
|
93
151
|
|
|
94
|
-
|
|
152
|
+
Verifies:
|
|
153
|
+
- ✅ REST API is accessible
|
|
154
|
+
- ✅ Credentials are valid
|
|
155
|
+
- ✅ User has `install_plugins` capability
|
|
156
|
+
- ✅ Receiver plugin is active
|
|
95
157
|
|
|
96
|
-
|
|
97
|
-
| --- | --- |
|
|
98
|
-
| `plugship init` | Configure a new WordPress site |
|
|
99
|
-
| `plugship deploy` | Deploy the plugin from the current directory |
|
|
100
|
-
| `plugship deploy --dry-run` | Preview deploy without uploading |
|
|
101
|
-
| `plugship deploy --all` | Deploy to all configured sites |
|
|
102
|
-
| `plugship status` | Check site connection and receiver status |
|
|
103
|
-
| `plugship sites list` | List all saved sites |
|
|
104
|
-
| `plugship sites remove <name>` | Remove a saved site |
|
|
105
|
-
| `plugship sites set-default <name>` | Set the default site |
|
|
106
|
-
| `plugship ignore` | Create `.plugshipignore` with default template |
|
|
107
|
-
| `plugship ignore <patterns...>` | Add patterns to `.plugshipignore` |
|
|
108
|
-
| `plugship --help` | Show help |
|
|
109
|
-
| `plugship --version` | Show version |
|
|
158
|
+
---
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
### `plugship sites`
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
Manage your saved sites.
|
|
114
163
|
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
164
|
+
```bash
|
|
165
|
+
# List all configured sites
|
|
166
|
+
plugship sites list
|
|
167
|
+
|
|
168
|
+
# Set the default site
|
|
169
|
+
plugship sites set-default production
|
|
170
|
+
|
|
171
|
+
# Remove a site
|
|
172
|
+
plugship sites remove staging
|
|
122
173
|
```
|
|
123
174
|
|
|
124
|
-
|
|
175
|
+
---
|
|
125
176
|
|
|
126
|
-
|
|
177
|
+
### `plugship ignore`
|
|
127
178
|
|
|
128
|
-
|
|
179
|
+
Manage file exclusions for deployment.
|
|
129
180
|
|
|
130
181
|
```bash
|
|
182
|
+
# Create .plugshipignore with default template
|
|
131
183
|
plugship ignore
|
|
132
|
-
```
|
|
133
184
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
plugship ignore "src/**" "*.map" composer.json
|
|
185
|
+
# Add specific patterns
|
|
186
|
+
plugship ignore "src/**" "*.map" "composer.json"
|
|
138
187
|
```
|
|
139
188
|
|
|
140
|
-
|
|
189
|
+
Creates a `.plugshipignore` file in your plugin directory. Example:
|
|
141
190
|
|
|
142
191
|
```
|
|
143
192
|
# .plugshipignore
|
|
144
193
|
src/**
|
|
145
|
-
|
|
194
|
+
*.map
|
|
146
195
|
webpack.config.js
|
|
147
196
|
package.json
|
|
148
197
|
package-lock.json
|
|
149
198
|
composer.json
|
|
150
|
-
composer.lock
|
|
151
|
-
*.map
|
|
152
199
|
```
|
|
153
200
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
**Already excluded by default:**
|
|
202
|
+
`node_modules`, `.git`, `.env`, `*.log`, `.vscode`, `.idea`, `tests`, `.github`, `build`
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 💡 Usage Examples
|
|
207
|
+
|
|
208
|
+
### Multi-Site Workflow
|
|
209
|
+
|
|
210
|
+
Configure multiple environments:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
plugship init # Add production
|
|
214
|
+
plugship init # Add staging
|
|
215
|
+
plugship init # Add local test site
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Deploy to each:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
plugship deploy --site staging # Test on staging first
|
|
222
|
+
plugship deploy --site production # Then push to prod
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or deploy everywhere at once:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
plugship deploy --all
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Preview Before Deploy
|
|
234
|
+
|
|
235
|
+
See what would be deployed without uploading:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
plugship deploy --dry-run
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Output shows:
|
|
242
|
+
- Plugin name, version, slug
|
|
243
|
+
- ZIP file size
|
|
244
|
+
- Target site(s)
|
|
245
|
+
- Activation setting
|
|
158
246
|
|
|
159
|
-
|
|
247
|
+
---
|
|
160
248
|
|
|
161
|
-
|
|
249
|
+
### Exclude Dev Files
|
|
162
250
|
|
|
163
|
-
|
|
251
|
+
Auto-suggest on first deploy, or create manually:
|
|
164
252
|
|
|
165
|
-
|
|
253
|
+
```bash
|
|
254
|
+
plugship ignore
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Edit `.plugshipignore` to add project-specific exclusions:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
# Custom exclusions
|
|
261
|
+
assets/src/**
|
|
262
|
+
*.scss
|
|
263
|
+
*.ts
|
|
264
|
+
tsconfig.json
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 🔧 How It Works
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
┌──────────────┐
|
|
273
|
+
│ Your Plugin │
|
|
274
|
+
│ Directory │
|
|
275
|
+
└──────┬───────┘
|
|
276
|
+
│
|
|
277
|
+
│ plugship deploy
|
|
278
|
+
▼
|
|
279
|
+
┌──────────────┐
|
|
280
|
+
│ ZIP File │ (excludes dev files)
|
|
281
|
+
└──────┬───────┘
|
|
282
|
+
│
|
|
283
|
+
│ Upload via REST API
|
|
284
|
+
▼
|
|
285
|
+
┌──────────────────────┐
|
|
286
|
+
│ WordPress Site │
|
|
287
|
+
│ (plugship-receiver) │
|
|
288
|
+
└──────┬───────────────┘
|
|
289
|
+
│
|
|
290
|
+
│ Install/Update
|
|
291
|
+
▼
|
|
292
|
+
┌──────────────┐
|
|
293
|
+
│ Plugin │
|
|
294
|
+
│ Active! │
|
|
295
|
+
└──────────────┘
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The [plugship-receiver](https://github.com/shamim0902/plugship-receiver) plugin adds two REST endpoints:
|
|
299
|
+
|
|
300
|
+
- `GET /wp-json/plugship/v1/status` — Health check
|
|
301
|
+
- `POST /wp-json/plugship/v1/deploy` — Accept plugin ZIP upload
|
|
302
|
+
|
|
303
|
+
WordPress's native `Plugin_Upgrader` with `overwrite_package => true` handles installation. Existing plugins are replaced automatically.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## ⚙️ Configuration
|
|
308
|
+
|
|
309
|
+
Config file: `~/.plugship/config.json`
|
|
166
310
|
|
|
167
311
|
```json
|
|
168
312
|
{
|
|
169
|
-
"defaultSite": "
|
|
313
|
+
"defaultSite": "production",
|
|
170
314
|
"sites": {
|
|
315
|
+
"production": {
|
|
316
|
+
"url": "https://example.com",
|
|
317
|
+
"username": "admin",
|
|
318
|
+
"appPassword": "xxxx xxxx xxxx xxxx"
|
|
319
|
+
},
|
|
171
320
|
"staging": {
|
|
172
321
|
"url": "https://staging.example.com",
|
|
173
322
|
"username": "admin",
|
|
@@ -177,15 +326,120 @@ Site credentials are stored in `~/.plugship/config.json` with `0600` file permis
|
|
|
177
326
|
}
|
|
178
327
|
```
|
|
179
328
|
|
|
180
|
-
|
|
329
|
+
**File permissions:** `0600` (only you can read/write)
|
|
181
330
|
|
|
182
|
-
|
|
331
|
+
---
|
|
183
332
|
|
|
184
|
-
|
|
185
|
-
|
|
333
|
+
## 🔒 Security
|
|
334
|
+
|
|
335
|
+
- ✅ **Application Passwords only** — never uses your main WordPress password
|
|
336
|
+
- ✅ **Local storage** — credentials stored in `~/.plugship/config.json` with restricted permissions
|
|
337
|
+
- ✅ **Capability checks** — only users with `install_plugins` can deploy
|
|
338
|
+
- ✅ **ZIP validation** — receiver plugin validates MIME type and ZIP integrity
|
|
339
|
+
- ✅ **Path traversal protection** — rejects ZIPs with malicious entries
|
|
340
|
+
- ✅ **50 MB upload limit** — prevents abuse
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 🛠️ Requirements
|
|
345
|
+
|
|
346
|
+
- **Node.js** 18 or higher
|
|
347
|
+
- **WordPress** 5.8 or higher
|
|
348
|
+
- **Admin account** with Application Passwords enabled
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## ❓ Troubleshooting
|
|
353
|
+
|
|
354
|
+
### "Receiver plugin not found"
|
|
355
|
+
|
|
356
|
+
**Problem:** The plugship-receiver plugin isn't active on your WordPress site.
|
|
357
|
+
|
|
358
|
+
**Solution:**
|
|
359
|
+
1. Download: https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip
|
|
360
|
+
2. Upload via **Plugins > Add New > Upload Plugin**
|
|
361
|
+
3. Activate **PlugShip Receiver**
|
|
362
|
+
4. Run `plugship status` to verify
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### "Authentication failed"
|
|
367
|
+
|
|
368
|
+
**Problem:** Your Application Password is incorrect or expired.
|
|
369
|
+
|
|
370
|
+
**Solution:**
|
|
371
|
+
1. Go to **Users > Profile** in WordPress admin
|
|
372
|
+
2. Delete the old Application Password
|
|
373
|
+
3. Create a new one
|
|
374
|
+
4. Run `plugship init` again and paste the new password
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### "Cannot reach REST API"
|
|
379
|
+
|
|
380
|
+
**Problem:** The WordPress REST API isn't accessible.
|
|
381
|
+
|
|
382
|
+
**Solution:**
|
|
383
|
+
1. Check that `https://yoursite.com/wp-json/` loads in your browser
|
|
384
|
+
2. Temporarily disable security plugins (Wordfence, iThemes, etc.)
|
|
385
|
+
3. Check hosting firewall rules
|
|
386
|
+
4. Verify mod_rewrite is enabled (permalinks must work)
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### Deploy fails with "permission denied"
|
|
391
|
+
|
|
392
|
+
**Problem:** User doesn't have `install_plugins` capability.
|
|
393
|
+
|
|
394
|
+
**Solution:**
|
|
395
|
+
- Ensure you're using an **Administrator** account
|
|
396
|
+
- Other roles (Editor, Author, etc.) can't install plugins
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 📖 Plugin Detection
|
|
401
|
+
|
|
402
|
+
PlugShip detects your plugin by scanning `.php` files in the current directory for WordPress plugin headers:
|
|
403
|
+
|
|
404
|
+
```php
|
|
405
|
+
<?php
|
|
406
|
+
/**
|
|
407
|
+
* Plugin Name: My Awesome Plugin
|
|
408
|
+
* Version: 1.0.0
|
|
409
|
+
* Text Domain: my-awesome-plugin
|
|
410
|
+
*/
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- The `Text Domain` is used as the plugin slug
|
|
414
|
+
- If no `Text Domain` is found, the slug is derived from the plugin name
|
|
415
|
+
- Version is read from the header and displayed during deploy
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 🌐 Links
|
|
420
|
+
|
|
421
|
+
- **npm package:** https://www.npmjs.com/package/plugship
|
|
422
|
+
- **Receiver plugin:** https://github.com/shamim0902/plugship-receiver
|
|
423
|
+
- **Report issues:** https://github.com/shamim0902/plugship/issues
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 📄 License
|
|
428
|
+
|
|
429
|
+
MIT © [shamim0902](https://github.com/shamim0902)
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 🙌 Contributing
|
|
434
|
+
|
|
435
|
+
Contributions are welcome! Please open an issue or PR.
|
|
186
436
|
|
|
187
|
-
|
|
437
|
+
1. Fork the repo
|
|
438
|
+
2. Create a feature branch: `git checkout -b feature/my-feature`
|
|
439
|
+
3. Commit your changes: `git commit -am 'Add my feature'`
|
|
440
|
+
4. Push: `git push origin feature/my-feature`
|
|
441
|
+
5. Open a Pull Request
|
|
188
442
|
|
|
189
|
-
|
|
443
|
+
---
|
|
190
444
|
|
|
191
|
-
|
|
445
|
+
**Made with ❤️ for the WordPress community**
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
2
8
|
|
|
3
9
|
const program = new Command();
|
|
4
10
|
|
|
5
11
|
program
|
|
6
12
|
.name('plugship')
|
|
7
13
|
.description('Deploy local WordPress plugins to remote sites')
|
|
8
|
-
.version(
|
|
14
|
+
.version(pkg.version);
|
|
9
15
|
|
|
10
16
|
program
|
|
11
17
|
.command('init')
|
package/src/commands/ignore.js
CHANGED
package/src/lib/constants.js
CHANGED
|
@@ -22,17 +22,10 @@ export const PLUGIN_HEADER_FIELDS = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export const DEFAULT_EXCLUDES = [
|
|
25
|
+
'.*',
|
|
25
26
|
'node_modules/**',
|
|
26
|
-
'.git/**',
|
|
27
|
-
'.DS_Store',
|
|
28
|
-
'.env',
|
|
29
27
|
'*.log',
|
|
30
|
-
'.vscode/**',
|
|
31
|
-
'.idea/**',
|
|
32
28
|
'tests/**',
|
|
33
29
|
'phpunit.xml',
|
|
34
|
-
'.phpunit.result.cache',
|
|
35
|
-
'.github/**',
|
|
36
30
|
'build/**',
|
|
37
|
-
'.plugshipignore',
|
|
38
31
|
];
|
package/src/lib/deployer.js
CHANGED
|
@@ -138,24 +138,98 @@ export async function deploy({ siteName, activate = true, dryRun = false, all =
|
|
|
138
138
|
// Upload
|
|
139
139
|
s.start('Uploading plugin...');
|
|
140
140
|
let result;
|
|
141
|
+
let uploadWarnings = null;
|
|
141
142
|
try {
|
|
142
143
|
result = await api.deployPlugin(zipPath, `${plugin.slug}.zip`);
|
|
143
|
-
s.succeed('Plugin uploaded and installed');
|
|
144
144
|
} catch (err) {
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
// Non-JSON response — plugin might still have installed
|
|
146
|
+
if (err.body && err.body.rawWarnings) {
|
|
147
|
+
s.succeed('Plugin uploaded');
|
|
148
|
+
uploadWarnings = err.body.rawWarnings;
|
|
149
|
+
} else {
|
|
150
|
+
s.fail('Upload failed');
|
|
151
|
+
if (targets.length > 1) {
|
|
152
|
+
logger.error(`Deploy to ${site.name} failed: ${err.message}`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
throw new DeployError(`Upload failed: ${err.message}`);
|
|
149
156
|
}
|
|
150
|
-
throw new DeployError(`Deploy failed: ${err.message}`);
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
// If we got warnings instead of clean JSON, verify installation via WP API
|
|
160
|
+
if (uploadWarnings) {
|
|
161
|
+
s.start('Verifying installation...');
|
|
162
|
+
try {
|
|
163
|
+
const pluginInfo = await api.getPlugin(`${plugin.slug}/${plugin.slug}`);
|
|
164
|
+
s.succeed('Plugin installed successfully');
|
|
165
|
+
result = {
|
|
166
|
+
success: true,
|
|
167
|
+
name: pluginInfo.name || plugin.name,
|
|
168
|
+
version: pluginInfo.version || plugin.version,
|
|
169
|
+
activated: pluginInfo.status === 'active',
|
|
170
|
+
};
|
|
171
|
+
for (const w of uploadWarnings) {
|
|
172
|
+
logger.warn(w);
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Try alternative slug format (slug/main-file)
|
|
176
|
+
try {
|
|
177
|
+
const plugins = await api.getPlugins();
|
|
178
|
+
const found = plugins.find((p) => p.textdomain === plugin.slug || p.plugin.startsWith(plugin.slug + '/'));
|
|
179
|
+
if (found) {
|
|
180
|
+
s.succeed('Plugin installed successfully');
|
|
181
|
+
result = {
|
|
182
|
+
success: true,
|
|
183
|
+
name: found.name || plugin.name,
|
|
184
|
+
version: found.version || plugin.version,
|
|
185
|
+
activated: found.status === 'active',
|
|
186
|
+
};
|
|
187
|
+
for (const w of uploadWarnings) {
|
|
188
|
+
logger.warn(w);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
s.fail('Installation could not be verified');
|
|
192
|
+
for (const w of uploadWarnings) {
|
|
193
|
+
logger.error(w);
|
|
194
|
+
}
|
|
195
|
+
if (targets.length > 1) continue;
|
|
196
|
+
throw new DeployError('Plugin installation could not be verified.');
|
|
197
|
+
}
|
|
198
|
+
} catch (verifyErr) {
|
|
199
|
+
if (verifyErr instanceof DeployError) throw verifyErr;
|
|
200
|
+
s.fail('Installation could not be verified');
|
|
201
|
+
for (const w of uploadWarnings) {
|
|
202
|
+
logger.error(w);
|
|
203
|
+
}
|
|
204
|
+
if (targets.length > 1) continue;
|
|
205
|
+
throw new DeployError('Plugin installation could not be verified.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Clean JSON response
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
s.fail('Installation failed');
|
|
212
|
+
logger.error('Plugin uploaded but installation failed.');
|
|
213
|
+
if (targets.length > 1) continue;
|
|
214
|
+
throw new DeployError('Installation failed on remote server.');
|
|
215
|
+
}
|
|
216
|
+
s.succeed('Plugin uploaded and installed');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Show warnings if any
|
|
220
|
+
if (result.warnings) {
|
|
221
|
+
logger.warn(`Warning: ${result.warnings}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Activation status
|
|
153
225
|
if (result.activated) {
|
|
154
226
|
logger.success(`Plugin "${result.name || plugin.name}" v${result.version || plugin.version} is active on ${site.url}`);
|
|
155
227
|
} else {
|
|
156
228
|
logger.success(`Plugin "${result.name || plugin.name}" v${result.version || plugin.version} installed on ${site.url}`);
|
|
157
|
-
if (activate &&
|
|
158
|
-
logger.
|
|
229
|
+
if (activate && result.activation_error) {
|
|
230
|
+
logger.error(`Activation failed: ${result.activation_error}`);
|
|
231
|
+
} else if (activate) {
|
|
232
|
+
logger.warn('Plugin installed but not activated. It may have errors — check WordPress admin.');
|
|
159
233
|
}
|
|
160
234
|
}
|
|
161
235
|
}
|
package/src/lib/wordpress-api.js
CHANGED
|
@@ -88,14 +88,53 @@ export class WordPressApi {
|
|
|
88
88
|
body: form.getBuffer(),
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
const
|
|
92
|
-
const body = contentType.includes('application/json') ? await res.json() : await res.text();
|
|
91
|
+
const rawBody = await res.text();
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// Try to extract JSON from response (may have HTML errors prepended)
|
|
94
|
+
const body = this._extractJson(rawBody);
|
|
95
|
+
|
|
96
|
+
if (body) {
|
|
97
|
+
if (!res.ok && body.message) {
|
|
98
|
+
throw new ApiError(body.message, res.status, body);
|
|
99
|
+
}
|
|
100
|
+
return body;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
// No valid JSON found — response is pure HTML/error
|
|
104
|
+
// Extract readable error from HTML
|
|
105
|
+
const warnings = this._extractHtmlErrors(rawBody);
|
|
106
|
+
throw new ApiError(
|
|
107
|
+
'non_json_response',
|
|
108
|
+
res.status,
|
|
109
|
+
{ rawWarnings: warnings }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_extractJson(text) {
|
|
114
|
+
// Try direct parse first
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
// JSON might be buried after HTML errors — find it
|
|
119
|
+
const jsonStart = text.indexOf('{"');
|
|
120
|
+
if (jsonStart > 0) {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(text.slice(jsonStart));
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_extractHtmlErrors(html) {
|
|
132
|
+
const errors = [];
|
|
133
|
+
const regex = /<b>(Warning|Fatal error|Parse error|Notice)<\/b>:\s*(.*?)<br/gi;
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = regex.exec(html)) !== null) {
|
|
136
|
+
errors.push(match[1] + ': ' + match[2].replace(/<[^>]+>/g, '').trim());
|
|
137
|
+
}
|
|
138
|
+
return errors.length > 0 ? errors : ['Server returned non-JSON response'];
|
|
100
139
|
}
|
|
101
140
|
}
|
package/src/lib/zipper.js
CHANGED
|
@@ -61,5 +61,10 @@ function matchGlob(filePath, pattern) {
|
|
|
61
61
|
if (pattern.startsWith('*.')) {
|
|
62
62
|
return filePath.endsWith(pattern.slice(1));
|
|
63
63
|
}
|
|
64
|
+
// Dotfile pattern — match root-level files/dirs starting with .
|
|
65
|
+
if (pattern === '.*') {
|
|
66
|
+
const firstSegment = filePath.split('/')[0];
|
|
67
|
+
return firstSegment.startsWith('.');
|
|
68
|
+
}
|
|
64
69
|
return filePath === pattern;
|
|
65
70
|
}
|