plugship 1.0.2 → 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
@@ -2,119 +2,200 @@
2
2
 
3
3
  > Deploy WordPress plugins from your terminal to any WordPress site instantly.
4
4
 
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)
7
+
5
8
  A simple CLI tool to deploy local WordPress plugins to remote WordPress sites. No FTP, no cPanel — just `plugship deploy`.
6
9
 
7
- ## Quick Start
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
8
26
 
9
- ### 1. Install
27
+ ### Global Install (Recommended)
10
28
 
11
29
  ```bash
12
30
  npm install -g plugship
13
31
  ```
14
32
 
15
- ### 2. Install Receiver Plugin
33
+ ### Verify Installation
34
+
35
+ ```bash
36
+ plugship --version
37
+ ```
38
+
39
+ ---
40
+
41
+ ## 🚀 Quick Start
16
42
 
17
- Download and install the companion plugin on your WordPress site:
43
+ ### Step 1: Install the Receiver Plugin
18
44
 
19
- **[Download plugship-receiver.zip](https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip)**
45
+ The `plugship-receiver` plugin must be installed on your WordPress site first. It adds secure REST endpoints to receive plugin uploads.
46
+
47
+ **[Download plugship-receiver.zip →](https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip)**
20
48
 
21
49
  1. Go to **Plugins > Add New > Upload Plugin** in WordPress admin
22
- 2. Upload the ZIP file
50
+ 2. Upload `plugship-receiver.zip`
23
51
  3. Activate **PlugShip Receiver**
24
52
 
25
- ### 3. Configure Your Site
53
+ **[View receiver plugin source →](https://github.com/shamim0902/plugship-receiver)**
54
+
55
+ ### Step 2: Create an Application Password
56
+
57
+ 1. Go to **Users > Profile** in WordPress admin
58
+ 2. Scroll to **Application Passwords**
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/)**
64
+
65
+ ### Step 3: Configure Your First Site
26
66
 
27
67
  ```bash
28
68
  plugship init
29
69
  ```
30
70
 
31
71
  You'll be prompted for:
32
- - Site alias (e.g., "production")
33
- - WordPress site URL
34
- - Admin username
35
- - [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) (create one in Users > Profile)
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
36
76
 
37
- ### 4. Deploy
77
+ ### Step 4: Deploy Your Plugin
38
78
 
39
- Navigate to your plugin directory and run:
79
+ Navigate to your WordPress plugin directory:
40
80
 
41
81
  ```bash
82
+ cd my-awesome-plugin/
42
83
  plugship deploy
43
84
  ```
44
85
 
45
- Done! Your plugin is deployed and activated.
86
+ Done! Your plugin is deployed and activated on the remote site.
46
87
 
47
88
  ---
48
89
 
49
- ## Commands
90
+ ## 📚 Commands
50
91
 
51
92
  ### `plugship deploy`
52
93
 
53
94
  Deploy the plugin from the current directory.
54
95
 
55
96
  ```bash
56
- plugship deploy # Deploy to default site
57
- plugship deploy --site staging # Deploy to specific site
58
- plugship deploy --all # Deploy to all configured sites
59
- plugship deploy --dry-run # Preview without uploading
60
- plugship deploy --no-activate # Deploy without activating
97
+ # Deploy to default site
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
61
111
  ```
62
112
 
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`)
119
+
120
+ ---
121
+
63
122
  ### `plugship init`
64
123
 
65
- Add a new WordPress site.
124
+ Add a new WordPress site to your configuration.
66
125
 
67
126
  ```bash
68
127
  plugship init
69
128
  ```
70
129
 
130
+ Interactive prompts guide you through:
131
+ - Site alias
132
+ - URL
133
+ - Username
134
+ - Application Password
135
+
136
+ The CLI automatically tests the connection and verifies the receiver plugin is active.
137
+
138
+ ---
139
+
71
140
  ### `plugship status`
72
141
 
73
142
  Check if a site is ready for deployment.
74
143
 
75
144
  ```bash
76
- plugship status # Check default site
77
- plugship status --site staging # Check specific site
145
+ # Check default site
146
+ plugship status
147
+
148
+ # Check a specific site
149
+ plugship status --site staging
78
150
  ```
79
151
 
152
+ Verifies:
153
+ - ✅ REST API is accessible
154
+ - ✅ Credentials are valid
155
+ - ✅ User has `install_plugins` capability
156
+ - ✅ Receiver plugin is active
157
+
158
+ ---
159
+
80
160
  ### `plugship sites`
81
161
 
82
162
  Manage your saved sites.
83
163
 
84
164
  ```bash
85
- plugship sites list # List all sites
86
- plugship sites set-default staging # Set default site
87
- plugship sites remove staging # Remove a site
88
- ```
165
+ # List all configured sites
166
+ plugship sites list
89
167
 
90
- ### `plugship ignore`
168
+ # Set the default site
169
+ plugship sites set-default production
91
170
 
92
- Exclude files from deployment.
93
-
94
- ```bash
95
- plugship ignore # Create .plugshipignore template
96
- plugship ignore "src/**" "*.map" # Add patterns
171
+ # Remove a site
172
+ plugship sites remove staging
97
173
  ```
98
174
 
99
175
  ---
100
176
 
101
- ## Ignoring Files
177
+ ### `plugship ignore`
102
178
 
103
- Create a `.plugshipignore` file to exclude files from deployment:
179
+ Manage file exclusions for deployment.
104
180
 
105
181
  ```bash
182
+ # Create .plugshipignore with default template
106
183
  plugship ignore
184
+
185
+ # Add specific patterns
186
+ plugship ignore "src/**" "*.map" "composer.json"
107
187
  ```
108
188
 
109
- This creates a template with common exclusions. Edit it to add your own:
189
+ Creates a `.plugshipignore` file in your plugin directory. Example:
110
190
 
111
191
  ```
112
192
  # .plugshipignore
113
193
  src/**
114
194
  *.map
195
+ webpack.config.js
115
196
  package.json
197
+ package-lock.json
116
198
  composer.json
117
- webpack.config.js
118
199
  ```
119
200
 
120
201
  **Already excluded by default:**
@@ -122,64 +203,108 @@ webpack.config.js
122
203
 
123
204
  ---
124
205
 
125
- ## How It Works
206
+ ## 💡 Usage Examples
126
207
 
127
- 1. **Detects your plugin** from the WordPress plugin header in your PHP files
128
- 2. **Creates a ZIP** with only the files you need (excludes dev files)
129
- 3. **Uploads via REST API** to the WordPress site using the receiver plugin
130
- 4. **Installs and activates** the plugin automatically
208
+ ### Multi-Site Workflow
131
209
 
132
- The [plugship-receiver](https://github.com/shamim0902/plugship-receiver) plugin adds secure REST endpoints to accept the upload. Only admin users with Application Passwords can deploy.
210
+ Configure multiple environments:
133
211
 
134
- ---
135
-
136
- ## Examples
212
+ ```bash
213
+ plugship init # Add production
214
+ plugship init # Add staging
215
+ plugship init # Add local test site
216
+ ```
137
217
 
138
- ### Deploy to Staging
218
+ Deploy to each:
139
219
 
140
220
  ```bash
141
- cd my-plugin/
142
- plugship deploy --site staging
221
+ plugship deploy --site staging # Test on staging first
222
+ plugship deploy --site production # Then push to prod
143
223
  ```
144
224
 
145
- ### Deploy to All Sites
225
+ Or deploy everywhere at once:
146
226
 
147
227
  ```bash
148
228
  plugship deploy --all
149
229
  ```
150
230
 
151
- ### Preview What Would Be Deployed
231
+ ---
232
+
233
+ ### Preview Before Deploy
234
+
235
+ See what would be deployed without uploading:
152
236
 
153
237
  ```bash
154
238
  plugship deploy --dry-run
155
239
  ```
156
240
 
157
- ### Check Connection Before Deploying
241
+ Output shows:
242
+ - Plugin name, version, slug
243
+ - ZIP file size
244
+ - Target site(s)
245
+ - Activation setting
246
+
247
+ ---
248
+
249
+ ### Exclude Dev Files
250
+
251
+ Auto-suggest on first deploy, or create manually:
158
252
 
159
253
  ```bash
160
- plugship status
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
161
265
  ```
162
266
 
163
267
  ---
164
268
 
165
- ## Requirements
269
+ ## 🔧 How It Works
166
270
 
167
- - **Node.js** 18 or higher
168
- - **WordPress** 5.8 or higher
169
- - **Admin account** with Application Passwords enabled
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
+ ```
170
297
 
171
- ---
298
+ The [plugship-receiver](https://github.com/shamim0902/plugship-receiver) plugin adds two REST endpoints:
172
299
 
173
- ## Security
300
+ - `GET /wp-json/plugship/v1/status` — Health check
301
+ - `POST /wp-json/plugship/v1/deploy` — Accept plugin ZIP upload
174
302
 
175
- - All credentials are stored locally in `~/.plugship/config.json` with `0600` permissions
176
- - Uses WordPress Application Passwords (not your main password)
177
- - Only users with `install_plugins` capability can deploy
178
- - All uploads are authenticated via WordPress REST API
303
+ WordPress's native `Plugin_Upgrader` with `overwrite_package => true` handles installation. Existing plugins are replaced automatically.
179
304
 
180
305
  ---
181
306
 
182
- ## Configuration
307
+ ## ⚙️ Configuration
183
308
 
184
309
  Config file: `~/.plugship/config.json`
185
310
 
@@ -201,43 +326,120 @@ Config file: `~/.plugship/config.json`
201
326
  }
202
327
  ```
203
328
 
329
+ **File permissions:** `0600` (only you can read/write)
330
+
331
+ ---
332
+
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
+
204
350
  ---
205
351
 
206
- ## Troubleshooting
352
+ ## Troubleshooting
207
353
 
208
354
  ### "Receiver plugin not found"
209
355
 
210
- The plugship-receiver plugin isn't active on your WordPress site.
356
+ **Problem:** The plugship-receiver plugin isn't active on your WordPress site.
211
357
 
358
+ **Solution:**
212
359
  1. Download: https://github.com/shamim0902/plugship-receiver/releases/latest/download/plugship-receiver.zip
213
- 2. Upload and activate in WordPress admin
360
+ 2. Upload via **Plugins > Add New > Upload Plugin**
361
+ 3. Activate **PlugShip Receiver**
362
+ 4. Run `plugship status` to verify
363
+
364
+ ---
214
365
 
215
366
  ### "Authentication failed"
216
367
 
217
- Your Application Password is incorrect.
368
+ **Problem:** Your Application Password is incorrect or expired.
218
369
 
370
+ **Solution:**
219
371
  1. Go to **Users > Profile** in WordPress admin
220
- 2. Generate a new Application Password
221
- 3. Run `plugship init` again
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
+ ---
222
377
 
223
378
  ### "Cannot reach REST API"
224
379
 
225
- Your WordPress REST API isn't accessible.
380
+ **Problem:** The WordPress REST API isn't accessible.
226
381
 
227
- - Check that `https://yoursite.com/wp-json/` loads
228
- - Disable security plugins temporarily to test
229
- - Check for firewall/hosting restrictions
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)
230
387
 
231
388
  ---
232
389
 
233
- ## Links
390
+ ### Deploy fails with "permission denied"
234
391
 
235
- - [npm package](https://www.npmjs.com/package/plugship)
236
- - [Receiver plugin](https://github.com/shamim0902/plugship-receiver)
237
- - [Report issues](https://github.com/shamim0902/plugship/issues)
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
238
397
 
239
398
  ---
240
399
 
241
- ## License
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.
436
+
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
442
+
443
+ ---
242
444
 
243
- 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.2",
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
  }