plugship 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugship",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Deploy local WordPress plugins to remote sites from the command line",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@ const DEFAULT_TEMPLATE = `# .plugshipignore
10
10
  #
11
11
  # The following are always excluded by default (no need to list them):
12
12
  # node_modules, .git, .github, .DS_Store, .env, *.log,
13
- # .vscode, .idea, tests, phpunit.xml, build
13
+ # .vscode, .idea, tests, phpunit.xml, builds
14
14
 
15
15
  # Source files (uncomment as needed)
16
16
  # src/**
@@ -1,4 +1,4 @@
1
- import { input, password } from '@inquirer/prompts';
1
+ import { input, password, confirm } from '@inquirer/prompts';
2
2
  import chalk from 'chalk';
3
3
  import { addSite } from '../lib/config.js';
4
4
  import { WordPressApi } from '../lib/wordpress-api.js';
@@ -13,87 +13,119 @@ export async function initCommand() {
13
13
  validate: (v) => (v.trim() ? true : 'Required'),
14
14
  });
15
15
 
16
- const url = await input({
17
- message: 'WordPress site URL:',
18
- validate: (v) => {
19
- if (!v.trim()) return 'Required';
20
- try {
21
- const parsed = new URL(v);
22
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
23
- return 'URL must start with https:// or http://';
16
+ let siteUrl, username, appPassword, api;
17
+ let connected = false;
18
+
19
+ // Loop until connection + auth succeed
20
+ while (!connected) {
21
+ siteUrl = await input({
22
+ message: 'WordPress site URL:',
23
+ validate: (v) => {
24
+ if (!v.trim()) return 'Required';
25
+ try {
26
+ const parsed = new URL(v);
27
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
28
+ return 'URL must start with https:// or http://';
29
+ }
30
+ return true;
31
+ } catch {
32
+ return 'Invalid URL';
24
33
  }
25
- return true;
26
- } catch {
27
- return 'Invalid URL';
28
- }
29
- },
30
- });
34
+ },
35
+ });
31
36
 
32
- const username = await input({
33
- message: 'WordPress username:',
34
- validate: (v) => (v.trim() ? true : 'Required'),
35
- });
37
+ siteUrl = siteUrl.replace(/\/+$/, '');
36
38
 
37
- const appPassword = await password({
38
- message: 'Application password:',
39
- mask: '*',
40
- validate: (v) => (v.trim() ? true : 'Required'),
41
- });
39
+ // Test connection
40
+ const spin = logger.spinner('Testing connection to WordPress REST API...');
41
+ spin.start();
42
42
 
43
- const siteUrl = url.replace(/\/+$/, '');
44
- const api = new WordPressApi({ url: siteUrl, username, appPassword });
43
+ try {
44
+ const tempApi = new WordPressApi({ url: siteUrl, username: '', appPassword: '' });
45
+ await tempApi.testConnection();
46
+ spin.succeed('REST API is accessible');
47
+ } catch (err) {
48
+ spin.stop();
49
+ spin.clear();
50
+ logger.error('Cannot reach WordPress REST API');
51
+ logger.error(`Make sure ${siteUrl}/wp-json/ is accessible.\n ${err.message}`);
52
+ console.log('');
53
+ const retry = await confirm({ message: 'Try a different URL?', default: true });
54
+ if (!retry) return;
55
+ continue;
56
+ }
45
57
 
46
- // Test connection
47
- const spin = logger.spinner('Testing connection to WordPress REST API...');
48
- spin.start();
58
+ // Loop until auth succeeds or user quits
59
+ let authenticated = false;
60
+ while (!authenticated) {
61
+ username = await input({
62
+ message: 'WordPress username:',
63
+ validate: (v) => (v.trim() ? true : 'Required'),
64
+ });
49
65
 
50
- try {
51
- await api.testConnection();
52
- spin.succeed('REST API is accessible');
53
- } catch (err) {
54
- spin.fail('Cannot reach WordPress REST API');
55
- logger.error(`Make sure ${siteUrl}/wp-json/ is accessible.\n ${err.message}`);
56
- process.exitCode = 1;
57
- return;
58
- }
66
+ appPassword = await password({
67
+ message: 'Application password:',
68
+ mask: '*',
69
+ validate: (v) => (v.trim() ? true : 'Required'),
70
+ });
59
71
 
60
- // Test authentication
61
- spin.start('Verifying credentials...');
62
- try {
63
- const user = await api.testAuth();
64
- const caps = user.capabilities || {};
65
- if (!caps.install_plugins) {
66
- spin.fail('User does not have the "install_plugins" capability');
67
- logger.error('The user must be an Administrator to deploy plugins.');
68
- process.exitCode = 1;
69
- return;
72
+ api = new WordPressApi({ url: siteUrl, username, appPassword });
73
+
74
+ const spin2 = logger.spinner('Verifying credentials...');
75
+ spin2.start();
76
+ try {
77
+ const user = await api.testAuth();
78
+ const caps = user.capabilities || {};
79
+ if (!caps.install_plugins) {
80
+ spin2.stop();
81
+ spin2.clear();
82
+ logger.error('User does not have the "install_plugins" capability');
83
+ logger.error('The user must be an Administrator to deploy plugins.');
84
+ console.log('');
85
+ const retry = await confirm({ message: 'Try different credentials?', default: true });
86
+ if (!retry) return;
87
+ continue;
88
+ }
89
+ spin2.succeed(`Authenticated as "${user.name}"`);
90
+ authenticated = true;
91
+ } catch (err) {
92
+ spin2.stop();
93
+ spin2.clear();
94
+ logger.error(`Authentication failed`);
95
+ logger.error(`Check your username and application password.\n ${err.message}`);
96
+ console.log('');
97
+ const retry = await confirm({ message: 'Try again?', default: true });
98
+ if (!retry) return;
99
+ }
70
100
  }
71
- spin.succeed(`Authenticated as "${user.name}"`);
72
- } catch (err) {
73
- spin.fail('Authentication failed');
74
- logger.error(`Check your username and application password.\n ${err.message}`);
75
- process.exitCode = 1;
76
- return;
101
+
102
+ connected = true;
77
103
  }
78
104
 
79
105
  // Check receiver plugin
80
- spin.start('Checking for plugship-receiver plugin...');
81
- try {
82
- const status = await api.checkReceiver();
83
- spin.succeed(`Receiver plugin active (v${status.version})`);
84
- } catch {
85
- spin.warn('Receiver plugin not detected');
86
- console.log('');
87
- logger.warn(
88
- 'The plugship-receiver plugin must be installed and activated on your WordPress site.'
89
- );
90
- console.log(
91
- chalk.dim(
92
- ` 1. Download: ${RECEIVER_DOWNLOAD_URL}\n` +
93
- ' 2. Upload and activate in WordPress admin (Plugins > Add New > Upload Plugin)\n' +
94
- ' 3. Run "plugship init" again to verify\n'
95
- )
96
- );
106
+ let receiverActive = false;
107
+ while (!receiverActive) {
108
+ const spin = logger.spinner('Checking for plugship-receiver plugin...');
109
+ spin.start();
110
+ try {
111
+ const status = await api.checkReceiver();
112
+ spin.succeed(`Receiver plugin active (v${status.version})`);
113
+ receiverActive = true;
114
+ } catch {
115
+ spin.warn('Receiver plugin not detected');
116
+ console.log('');
117
+ logger.warn(
118
+ 'The plugship-receiver plugin must be installed and activated on your WordPress site.'
119
+ );
120
+ console.log(
121
+ chalk.dim(
122
+ ` 1. Download: ${RECEIVER_DOWNLOAD_URL}\n` +
123
+ ' 2. Upload and activate in WordPress admin (Plugins > Add New > Upload Plugin)\n'
124
+ )
125
+ );
126
+ const retry = await confirm({ message: 'Check again?', default: true });
127
+ if (!retry) break;
128
+ }
97
129
  }
98
130
 
99
131
  // Save config
@@ -103,5 +135,12 @@ export async function initCommand() {
103
135
  appPassword,
104
136
  });
105
137
 
106
- logger.success(`Site "${name.trim()}" saved and set as default.\n`);
138
+ logger.success(`Site "${name.trim()}" saved and set as default.`);
139
+ console.log(chalk.bold('\nNext steps:\n'));
140
+ console.log(` Navigate to your plugin directory and run:\n`);
141
+ console.log(chalk.cyan(` plugship deploy\n`));
142
+ console.log(chalk.dim(` Other useful commands:`));
143
+ console.log(chalk.dim(` plugship deploy --dry-run Preview without uploading`));
144
+ console.log(chalk.dim(` plugship deploy --site ${name.trim()} Deploy to this site`));
145
+ console.log(chalk.dim(` plugship ignore Set up file exclusions\n`));
107
146
  }
@@ -27,5 +27,5 @@ export const DEFAULT_EXCLUDES = [
27
27
  '*.log',
28
28
  'tests/**',
29
29
  'phpunit.xml',
30
- 'build/**',
30
+ 'builds/**',
31
31
  ];
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { access } from 'node:fs/promises';
2
+ import { access, stat } from 'node:fs/promises';
3
3
  import { select, confirm } from '@inquirer/prompts';
4
4
  import chalk from 'chalk';
5
5
  import { getSite, listSites } from './config.js';
@@ -83,12 +83,37 @@ export async function deploy({ siteName, activate = true, dryRun = false, all =
83
83
  // Detect plugin
84
84
  const plugin = await detectPlugin(cwd);
85
85
 
86
- // Build ZIP once (shared across all targets)
87
- const spin = logger.spinner('Creating ZIP archive...');
88
- spin.start();
89
- const { zipPath, size } = await createPluginZip(cwd, plugin.slug);
90
- const sizeMB = (size / 1024 / 1024).toFixed(2);
91
- spin.succeed(`ZIP created (${sizeMB} MB)`);
86
+ // Check for existing ZIP
87
+ const existingZipPath = join(cwd, 'builds', `${plugin.slug}.zip`);
88
+ let zipPath, size;
89
+
90
+ try {
91
+ const zipStat = await stat(existingZipPath);
92
+ const sizeMB = (zipStat.size / 1024 / 1024).toFixed(2);
93
+ logger.info(`Existing ZIP found: builds/${plugin.slug}.zip (${sizeMB} MB)`);
94
+ const action = await select({
95
+ message: 'What do you want to do?',
96
+ choices: [
97
+ { name: 'Use existing ZIP', value: 'existing' },
98
+ { name: 'Build a new ZIP', value: 'rebuild' },
99
+ ],
100
+ });
101
+ if (action === 'rebuild') {
102
+ const spin = logger.spinner('Creating ZIP archive...');
103
+ spin.start();
104
+ ({ zipPath, size } = await createPluginZip(cwd, plugin.slug));
105
+ spin.succeed(`ZIP created (${(size / 1024 / 1024).toFixed(2)} MB)`);
106
+ } else {
107
+ zipPath = existingZipPath;
108
+ size = zipStat.size;
109
+ logger.success(`Using existing ZIP (${sizeMB} MB)`);
110
+ }
111
+ } catch {
112
+ const spin = logger.spinner('Creating ZIP archive...');
113
+ spin.start();
114
+ ({ zipPath, size } = await createPluginZip(cwd, plugin.slug));
115
+ spin.succeed(`ZIP created (${(size / 1024 / 1024).toFixed(2)} MB)`);
116
+ }
92
117
 
93
118
  // Dry run — show summary and exit
94
119
  if (dryRun) {
package/src/lib/zipper.js CHANGED
@@ -21,7 +21,7 @@ async function loadIgnorePatterns(sourceDir) {
21
21
  }
22
22
 
23
23
  export async function createPluginZip(sourceDir, slug) {
24
- const buildDir = join(sourceDir, 'build');
24
+ const buildDir = join(sourceDir, 'builds');
25
25
  await mkdir(buildDir, { recursive: true });
26
26
  const zipName = `${slug}.zip`;
27
27
  const zipPath = join(buildDir, zipName);