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 CHANGED
@@ -1,173 +1,322 @@
1
1
  # plugship
2
2
 
3
- A CLI tool to deploy local WordPress plugins to remote WordPress sites.
3
+ > Deploy WordPress plugins from your terminal to any WordPress site instantly.
4
4
 
5
- ## Prerequisites
5
+ [![npm version](https://img.shields.io/npm/v/plugship.svg)](https://www.npmjs.com/package/plugship)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- - Node.js 18+
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
- ## Installation
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
- ## Setup
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
- ### 1. Install the Receiver Plugin
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
- The `plugship-receiver` companion plugin must be installed on your WordPress site. It adds a REST endpoint that accepts plugin ZIP uploads.
47
+ **[Download plugship-receiver.zip →](https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip)**
22
48
 
23
- 1. Download [plugship-receiver.zip](https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip) or get it from the [repo](https://github.com/shamim0902/plugship-receiver)
24
- 2. Go to **Plugins > Add New > Upload Plugin** in WordPress admin
25
- 3. Upload the ZIP and activate **PlugShip Receiver**
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
- ### 2. Create an Application Password
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 a name (e.g. "plugship") and click **Add New Application Password**
32
- 4. Copy the generated password
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. Configure a Site
65
+ ### Step 3: Configure Your First Site
35
66
 
36
67
  ```bash
37
68
  plugship init
38
69
  ```
39
70
 
40
- You will be prompted for:
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
- - **Site alias** — a short name for this site (e.g. "staging")
43
- - **Site URL** — your WordPress site URL (e.g. `https://example.com`)
44
- - **Username** — your WordPress admin username
45
- - **Application password** — the password from step 2
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
- The command will verify the connection, credentials, and receiver plugin status.
88
+ ---
48
89
 
49
- ## Usage
90
+ ## 📚 Commands
50
91
 
51
- ### Deploy a Plugin
92
+ ### `plugship deploy`
52
93
 
53
- Navigate to your WordPress plugin directory and run:
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
- This will:
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
- 1. Detect the plugin from PHP file headers
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
- If you have multiple sites configured, you will be prompted to select one.
122
+ ### `plugship init`
67
123
 
68
- #### Options
124
+ Add a new WordPress site to your configuration.
69
125
 
70
126
  ```bash
71
- plugship deploy --site <name> # Deploy to a specific site
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
- ### Check Site Status
130
+ Interactive prompts guide you through:
131
+ - Site alias
132
+ - URL
133
+ - Username
134
+ - Application Password
78
135
 
79
- Verify connection, credentials, and receiver plugin before deploying:
136
+ The CLI automatically tests the connection and verifies the receiver plugin is active.
80
137
 
81
- ```bash
82
- plugship status # Check default or select a site
83
- plugship status --site <name> # Check a specific site
84
- ```
138
+ ---
139
+
140
+ ### `plugship status`
85
141
 
86
- ### Manage Sites
142
+ Check if a site is ready for deployment.
87
143
 
88
144
  ```bash
89
- plugship sites list # List all saved sites
90
- plugship sites remove <name> # Remove a saved site
91
- plugship sites set-default <name> # Set the default site
145
+ # Check default site
146
+ plugship status
147
+
148
+ # Check a specific site
149
+ plugship status --site staging
92
150
  ```
93
151
 
94
- ## Commands
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
- | Command | Description |
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
- ## Plugin Detection
160
+ ### `plugship sites`
112
161
 
113
- The CLI detects your plugin by scanning `.php` files in the current directory for a standard WordPress plugin header:
162
+ Manage your saved sites.
114
163
 
115
- ```php
116
- <?php
117
- /**
118
- * Plugin Name: My Plugin
119
- * Version: 1.0.0
120
- * Text Domain: my-plugin
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
- The `Text Domain` is used as the plugin slug. If not provided, the slug is derived from the plugin name.
175
+ ---
125
176
 
126
- ## Ignoring Files
177
+ ### `plugship ignore`
127
178
 
128
- Use the `ignore` command to create a `.plugshipignore` file with a default template:
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
- Or add specific patterns directly:
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
- You can also manually create or edit `.plugshipignore` in your plugin directory to exclude files and folders from the deployment ZIP:
189
+ Creates a `.plugshipignore` file in your plugin directory. Example:
141
190
 
142
191
  ```
143
192
  # .plugshipignore
144
193
  src/**
145
- assets/scss/**
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
- - One pattern per line
155
- - Lines starting with `#` are comments
156
- - Blank lines are ignored
157
- - Supports `dir/**` (directory and contents), `*.ext` (extension match), and exact names
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
- The following are always excluded by default:
247
+ ---
160
248
 
161
- `node_modules`, `.git`, `.DS_Store`, `.env`, `*.log`, `.vscode`, `.idea`, `tests`, `phpunit.xml`, `.phpunit.result.cache`, `.github`, `build`
249
+ ### Exclude Dev Files
162
250
 
163
- ## Configuration
251
+ Auto-suggest on first deploy, or create manually:
164
252
 
165
- Site credentials are stored in `~/.plugship/config.json` with `0600` file permissions. The config file looks like:
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": "staging",
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
- ## How It Works
329
+ **File permissions:** `0600` (only you can read/write)
181
330
 
182
- The WordPress REST API does not support direct ZIP upload for plugin installation. The `plugship-receiver` companion plugin adds two custom endpoints:
331
+ ---
183
332
 
184
- - `GET /wp-json/plugship/v1/status` — Health check
185
- - `POST /wp-json/plugship/v1/deploy` — Accepts a ZIP file and installs it using WordPress's built-in `Plugin_Upgrader` with `overwrite_package => true`
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
- If the plugin already exists on the site, it is replaced with the uploaded version.
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
- ## License
443
+ ---
190
444
 
191
- MIT
445
+ **Made with ❤️ for the WordPress community**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugship",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Deploy local WordPress plugins to remote sites from the command line",
5
5
  "type": "module",
6
6
  "bin": {
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('1.0.0');
14
+ .version(pkg.version);
9
15
 
10
16
  program
11
17
  .command('init')
@@ -19,7 +19,6 @@ const DEFAULT_TEMPLATE = `# .plugshipignore
19
19
  # Build tools
20
20
  package.json
21
21
  package-lock.json
22
- composer.json
23
22
  composer.lock
24
23
  webpack.config.js
25
24
  `;
@@ -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
  ];
@@ -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
- s.fail('Upload failed');
146
- if (targets.length > 1) {
147
- logger.error(`Deploy to ${site.name} failed: ${err.message}`);
148
- continue;
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 && !result.activated) {
158
- logger.info('Plugin was not activated (may already be active or activation was skipped).');
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
  }
@@ -88,14 +88,53 @@ export class WordPressApi {
88
88
  body: form.getBuffer(),
89
89
  });
90
90
 
91
- const contentType = res.headers.get('content-type') || '';
92
- const body = contentType.includes('application/json') ? await res.json() : await res.text();
91
+ const rawBody = await res.text();
93
92
 
94
- if (!res.ok) {
95
- const msg = typeof body === 'object' && body.message ? body.message : `Upload failed (HTTP ${res.status})`;
96
- throw new ApiError(msg, res.status, body);
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
- return body;
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
  }