@caweb/cli 1.3.1 → 1.3.2

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.
@@ -149,11 +149,9 @@ export default async function start({
149
149
 
150
150
  // Create an Application Password for the user.
151
151
  /*
152
- const devAppPwd = await runCmd(
153
- 'php',
152
+ const devAppPwd = await runCLICmd(
153
+ 'wp',
154
154
  [
155
- path.join(projectPath, 'bin', 'wp-cli.phar'),
156
- `--ssh=docker:${path.basename(workDirectoryPath)}-cli-1`,
157
155
  `user application-password create 1 caweb`,
158
156
  '--porcelain'
159
157
  ]
package/commands/sync.js CHANGED
@@ -26,12 +26,14 @@ const errors = [];
26
26
  * @param {boolean} options.from Remote Site URL with current changes.
27
27
  * @param {boolean} options.to Destination Site URL that should be synced.
28
28
  * @param {boolean} options.debug True if debug mode is enabled.
29
+ * @param {Array} options.tax Taxonomy that should be synced.
29
30
  */
30
31
  export default async function sync({
31
32
  spinner,
32
33
  debug,
33
34
  from,
34
- to
35
+ to,
36
+ tax
35
37
  } ) {
36
38
 
37
39
  const localFile = path.join(appPath, 'caweb.json');
@@ -90,8 +92,16 @@ export default async function sync({
90
92
  'accept': '*/*'
91
93
  }
92
94
  }
93
-
95
+
96
+ let pages = [];
97
+ let posts = [];
98
+ let attachedMedia = [];
99
+ let featuredMedia = [];
100
+ let menus = [];
101
+ let menuNavItems = [];
102
+
94
103
  // get all settings
104
+ // site settings are always synced
95
105
  spinner.text = `Getting Site Settings from ${from.url}`;
96
106
  let settings = await getTaxonomies({
97
107
  ...fromOptions,
@@ -101,122 +111,163 @@ export default async function sync({
101
111
  ],
102
112
  }, 'settings')
103
113
 
104
- // get all pages/posts
105
- spinner.text = `Gathering all pages/posts from ${from.url}`;
106
- let pages = await getTaxonomies(fromOptions);
107
- let posts = await getTaxonomies(fromOptions, 'posts');
108
-
109
- spinner.text = `Collecting all attached/featured images from ${from.url}`;
110
114
  /**
111
- * Media Library Handling
112
- * 1) attached media items by default are only possible on pages.
113
- * 2) featured media by default are only possible on posts.
115
+ * If taxonomy is undefined then we sync everything, otherwise we only sync what's being requested.
116
+ * First we collect everything we need from the base, then we send the information to the target.
117
+ * If media is requested then we need to download posts/pages to get any attached/featured media.
114
118
  */
115
- const mediaFields = [
116
- 'id',
117
- 'source_url',
118
- 'title',
119
- 'caption',
120
- 'alt_text',
121
- 'date',
122
- 'mime_type'
123
- ];
124
- let attachedMedia = await getTaxonomies({
125
- ...fromOptions,
126
- fields: mediaFields,
127
- parent: pages.map((p) => { return p.id })
128
- },
129
- 'media'
130
- );
131
-
132
- let featuredMedia = await getTaxonomies({
133
- ...fromOptions,
134
- fields: mediaFields,
135
- include: posts.map((p) => { if(p.featured_media){return p.featured_media } })
136
- },
137
- 'media'
138
- );
139
-
140
- // before we can upload media files.
141
- for( let mediaObj of [].concat( attachedMedia, featuredMedia ) ){
142
- // generate a blob from the media object source_url.
143
- const mediaBlob = await axios.request(
144
- {
145
- ...fromOptions,
146
- url: mediaObj.source_url,
147
- responseType: 'arraybuffer'
148
- }
149
- )
150
- .then( (img) => { return new Blob([img.data]) })
151
- .catch( (error) => {
152
- errors.push(`${mediaObj.source_url} could not be downloaded.`)
153
- return false;
154
- });
119
+ // collect pages
120
+ if( undefined === tax || tax.includes('pages') || tax.includes('media')){
121
+ // get all pages/posts
122
+ spinner.text = `Gathering all pages from ${from.url}`;
123
+ pages = await getTaxonomies(fromOptions);
124
+ }
125
+
126
+ // collect posts
127
+ if( undefined === tax || tax.includes('posts')|| tax.includes('media')){
128
+ // get all pages/posts
129
+ spinner.text = `Gathering all posts from ${from.url}`;
130
+ posts = await getTaxonomies(fromOptions, 'posts');
131
+ }
132
+
133
+ // collect media
134
+ if( undefined === tax || tax.includes('media')){
135
+
136
+ spinner.text = `Collecting all attached/featured images from ${from.url}`;
137
+ /**
138
+ * Media Library Handling
139
+ * 1) attached media items by default are only possible on pages.
140
+ * 2) featured media by default are only possible on posts.
141
+ */
142
+ const mediaFields = [
143
+ 'id',
144
+ 'source_url',
145
+ 'title',
146
+ 'caption',
147
+ 'alt_text',
148
+ 'date',
149
+ 'mime_type'
150
+ ];
151
+ attachedMedia = await getTaxonomies({
152
+ ...fromOptions,
153
+ fields: mediaFields,
154
+ parent: pages.map((p) => { return p.id })
155
+ },
156
+ 'media'
157
+ );
158
+
159
+ featuredMedia = await getTaxonomies({
160
+ ...fromOptions,
161
+ fields: mediaFields,
162
+ include: posts.map((p) => { if(p.featured_media){return p.featured_media } })
163
+ },
164
+ 'media'
165
+ );
155
166
 
156
- if( mediaBlob ){
157
- mediaObj.data = mediaBlob;
167
+ // before we can upload media files.
168
+ for( let mediaObj of [].concat( attachedMedia, featuredMedia ) ){
169
+ // generate a blob from the media object source_url.
170
+ const mediaBlob = await axios.request(
171
+ {
172
+ ...fromOptions,
173
+ url: mediaObj.source_url,
174
+ responseType: 'arraybuffer'
175
+ }
176
+ )
177
+ .then( (img) => { return new Blob([img.data]) })
178
+ .catch( (error) => {
179
+ errors.push(`${mediaObj.source_url} could not be downloaded.`)
180
+ return false;
181
+ });
182
+
183
+ if( mediaBlob ){
184
+ mediaObj.data = mediaBlob;
185
+ }
158
186
  }
159
187
  }
160
-
161
- spinner.text = `Collecting assigned navigation menus from ${from.url}`;
162
- // get all menus and navigation links
163
- let menus = await getTaxonomies({
164
- ...fromOptions,
165
- fields: [
166
- 'id',
167
- 'description',
168
- 'name',
169
- 'slug',
170
- 'meta',
171
- 'locations'
172
- ]
173
- }, 'menus');
174
-
175
- let menuNavItems = await getTaxonomies(
176
- {
188
+
189
+ // collect menu and nav items.
190
+ if( undefined === tax || tax.includes('menus')){
191
+ spinner.text = `Collecting assigned navigation menus from ${from.url}`;
192
+ // get all menus and navigation links
193
+ menus = await getTaxonomies({
177
194
  ...fromOptions,
178
195
  fields: [
179
196
  'id',
180
- 'title',
181
- 'url',
182
- 'status',
183
- 'attr_title',
184
197
  'description',
185
- 'type',
186
- 'type_label',
187
- 'object',
188
- 'object_id',
189
- 'parent',
190
- 'menu_order',
191
- 'target',
192
- 'classes',
193
- 'xfn',
198
+ 'name',
199
+ 'slug',
194
200
  'meta',
195
- 'menus'
196
- ],
197
- menus: menus.map((menu) => { return menu.id; })
198
- },
199
- 'menu-items'
200
- )
201
+ 'locations'
202
+ ]
203
+ }, 'menus');
201
204
 
202
- // create all pages/posts
203
- spinner.text = `Creating all pages/posts to ${to.url}`;
204
- await createTaxonomies( pages, toOptions, 'pages', spinner );
205
- await createTaxonomies( posts, toOptions, 'posts', spinner );
205
+ menuNavItems = await getTaxonomies(
206
+ {
207
+ ...fromOptions,
208
+ fields: [
209
+ 'id',
210
+ 'title',
211
+ 'url',
212
+ 'status',
213
+ 'attr_title',
214
+ 'description',
215
+ 'type',
216
+ 'type_label',
217
+ 'object',
218
+ 'object_id',
219
+ 'parent',
220
+ 'menu_order',
221
+ 'target',
222
+ 'classes',
223
+ 'xfn',
224
+ 'meta',
225
+ 'menus'
226
+ ],
227
+ menus: menus.map((menu) => { return menu.id; })
228
+ },
229
+ 'menu-items'
230
+ )
231
+
232
+ }
233
+
234
+ /**
235
+ * Now we have all the data we can begin to create the taxonomies on the target.
236
+ *
237
+ * Creation order is important otherwise missing dependencies will cause errors to trigger.
238
+ * We create media first, then pages, then posts, then menus and navigation links.
239
+ */
206
240
 
207
241
  // create media attachments
208
- spinner.text = `Uploading media files to ${to.url}`;
209
- await createTaxonomies(
210
- [].concat( attachedMedia, featuredMedia ).filter((img) => { return img.data }),
211
- toOptions,
212
- 'media',
213
- spinner
214
- )
215
-
242
+ if( undefined === tax || tax.includes('media')){
243
+ spinner.text = `Uploading media files to ${to.url}`;
244
+ await createTaxonomies(
245
+ [].concat( attachedMedia, featuredMedia ).filter((img) => { return img.data }),
246
+ toOptions,
247
+ 'media',
248
+ spinner
249
+ )
250
+ }
251
+
252
+ // create pages
253
+ if( undefined === tax || tax.includes('pages')){
254
+ spinner.text = `Creating all pages to ${to.url}`;
255
+ await createTaxonomies( pages, toOptions, 'pages', spinner );
256
+ }
257
+
258
+ // create posts
259
+ if( undefined === tax || tax.includes('posts')){
260
+ spinner.text = `Creating all posts to ${to.url}`;
261
+ await createTaxonomies( posts, toOptions, 'posts', spinner );
262
+ }
263
+
216
264
  // create menus and navigation links
217
- spinner.text = `Reconstructing navigation menus to ${to.url}`;
218
- await createTaxonomies(menus, toOptions, 'menus', spinner);
219
- await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
265
+ if( undefined === tax || tax.includes('menus')){
266
+ spinner.text = `Reconstructing navigation menus to ${to.url}`;
267
+ await createTaxonomies(menus, toOptions, 'menus', spinner);
268
+ await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
269
+ }
270
+
220
271
 
221
272
  // update settings
222
273
  await createTaxonomies(settings, toOptions, 'settings', spinner);
package/commands/test.js CHANGED
@@ -2,24 +2,22 @@
2
2
  * External dependencies
3
3
  */
4
4
  import path from 'path';
5
- import axios from 'axios';
6
5
  import fs from 'fs';
7
6
  import loadConfig from '@wordpress/env/lib/config/load-config.js';
8
- import cp from 'child_process';
7
+ import os from 'os';
9
8
 
10
9
  //var WP = require('wp-cli');
11
10
  /**
12
11
  * Internal dependencies
13
12
  */
14
13
  import { runCLICmds, runCmd, projectPath } from '../lib/index.js';
14
+ import { CAWEB_OPTIONS, DIVI_OPTIONS } from '../lib/wordpress/index.js';
15
15
  /*
16
16
  import generateOverridesMD from '../admin.js';
17
- import { CAWEB_OPTIONS, DIVI_OPTIONS } from '../options.js';
18
17
  */
19
18
  /**
20
19
  * Promisified dependencies
21
20
  */
22
- import os from 'os';
23
21
 
24
22
  /**
25
23
  * Test code.
@@ -36,65 +34,21 @@ export default async function test({
36
34
  } ) {
37
35
 
38
36
  spinner.text = "Testing Code Functionality";
39
- const config = await loadConfig(path.resolve('.'));
40
-
41
- process.env.WP_CLI_CONFIG_PATH = path.join(config.workDirectoryPath, 'config.yml');
42
- // update post meta.
43
- let id = 35;
44
- let newId = 30
45
- let tbl = `wp_term_taxonomy`;
37
+ const config = await loadConfig(path.resolve('.'));
38
+ const { workDirectoryPath } = config;
39
+ process.env.WP_CLI_CONFIG_PATH = path.join(config.workDirectoryPath, 'config.yml');
46
40
 
47
- let result = await runCmd(
41
+ let cawebOptNames = CAWEB_OPTIONS;
42
+
43
+ let result = await runCmd(
48
44
  'php',
49
45
  [
50
- path.join(projectPath, 'bin', 'wp-cli.phar'),
51
- '--ssh=docker:' + path.basename(config.workDirectoryPath) + '-cli-1',
52
- `db query 'UPDATE ${tbl} SET term_taxonomy_id=${newId},term_id=${newId} WHERE term_id=${id}'`,
46
+ //'--info'
53
47
  ]
54
48
  )
55
49
 
56
- console.log( result );
57
- /*
58
- let q = `UPDATE wp_posts SET id=1 WHERE id=10 `;
59
- let u = 'http://danny.com/wp-json/wp/v2/pages/2827';
60
- let result = await axios.request({
61
- url: u,
62
- method: 'POST',
63
- data: {
64
- title: 'Audio',
65
- meta: {
66
- _et_pb_use_builder: 'on'
67
- }
68
- },
69
- headers: {
70
- Authorization: 'Basic ' + Buffer.from(`admin:9RUG nPuo u581 cY2A uSJd FE2P`).toString('base64')
71
- }
72
- })
73
- .then( async (res) => { return res.data; } )
74
- .catch( async (err) => { return err; } )
75
-
76
- */
77
- /*
78
- let result = await runCmd(
79
- 'php',
80
- [
81
- //'-r',
82
- path.join(projectPath, 'bin', 'wp-cli.phar'),
83
- '--ssh=' + `docker:${path.basename(config.workDirectoryPath)}-cli-1`,
84
- //'@dev',
85
- //`db query '${q}'`,
86
- 'post list',
87
- //'--info',
88
- '--skip-themes',
89
- //`--url=${new URL(u).origin}`
90
- // '--format=json'
91
- //'--skip-column-names',
92
- //'post list path=/nas/content/live/cawebdev/ --http=https://dev.sites.ca.gov --url=https://dev.sites.ca.gov',
93
- ],
94
- config
95
- )*/
50
+
51
+ console.log( 'out', result.stdout.toString() );
52
+ console.log( 'err', result.stderr.toString() );
96
53
 
97
- //console.log( 'Result', result.toString() );
98
- //console.log( 'Output', result.stdout.toString() );
99
- //console.log( 'Error', result.stderr.toString() );
100
54
  };
@@ -85,7 +85,7 @@ let webpackConfig = {
85
85
  devtool: false,
86
86
  output: {
87
87
  ...baseConfig.output,
88
- publicPath: `/`,
88
+ publicPath: `./`,
89
89
  clean: false
90
90
  },
91
91
  plugins: [
package/lib/cli.js CHANGED
@@ -259,17 +259,20 @@ export default function cli() {
259
259
  .description('Sync changes from one destination to another.')
260
260
  .argument('<from>', 'Target Site URL with current changes.')
261
261
  .argument('<to>', 'Destination Site URL that should be synced.')
262
+ .addOption(new Option(
263
+ '-t,--tax [tax...]',
264
+ 'Taxonomy that should be synced. Omitting this option will sync the full site.'
265
+ ).choices(['pages', 'posts', 'media', 'menus']))
262
266
  .allowUnknownOption(true)
263
267
  .action( withSpinner(env.sync) )
264
268
 
265
269
  // Test Command.
266
270
  // Ensure this is commented out.
267
- /*program.command('test')
271
+ program.command('test')
268
272
  .description('Test commands on a WordPress environment')
269
273
  //.addArgument(envArg)
270
274
  .allowUnknownOption(true)
271
275
  .action(withSpinner(env.test))
272
- */
273
276
 
274
277
  addWPEnvCommands();
275
278
 
package/lib/spinner.js CHANGED
@@ -61,15 +61,19 @@ const withSpinner =
61
61
  ){
62
62
 
63
63
  let { data, status } = error.response;
64
-
64
+ let msg = data.message || 'An unknown error occurred.';
65
+
65
66
  if( 401 === status ){
66
- data.message += `\nPlease check your ${terminalLink('Application Password', 'https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/')}`;
67
+ msg += `\nPlease check your ${terminalLink('Application Password', 'https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/')}`;
68
+ } else if( 403 === status ){
69
+ msg = 'Forbidden Request: A potentially unsafe operation has been detected in your request to this site';
67
70
  }
68
71
 
69
- spinner.fail( data.message )
72
+ spinner.fail( msg )
70
73
  process.exit( 1 );
71
74
 
72
75
 
76
+
73
77
  }else if( error ){
74
78
  // Error is an unknown error. That means there was a bug in our code.
75
79
  spinner.fail(
@@ -174,7 +174,7 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
174
174
  let url = `${request.url}${endpoint}/${tax}`;
175
175
  let existingID = false;
176
176
 
177
- // in order to maintain ID's, we have to use the cli, the REST API doesn't allow passing the id.
177
+ // The REST API doesn't allow passing the id, in order to maintain ID's we make an additional request to our plugin endpoint.
178
178
  if( obj.id ){
179
179
  // first we check if the ID exist
180
180
  let idExists = await axios.request(
@@ -203,7 +203,7 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
203
203
  }
204
204
 
205
205
  /**
206
- * if the ID doesn't exist we save it so we can update via CLI later on.
206
+ * if the ID doesn't exist we save it so we can make a request to our plugin endpoint.
207
207
  * if it does exist update endpoint url so we update the existing item.
208
208
  */
209
209
  if( ! idExists ){
@@ -232,6 +232,7 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
232
232
  // make WordPress REST API request.
233
233
  let results = await axios.request({
234
234
  ...request,
235
+ timeout: 7500,
235
236
  data: 'media' === tax ? createMediaItem(obj) : obj,
236
237
  url
237
238
  })
@@ -239,112 +240,22 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
239
240
 
240
241
 
241
242
  /**
242
- * if the obj had an existing ID that didn't exist
243
+ * if the obj had an existing ID that didn't exist we make a request to our plugin endpoint to update the IDs.
243
244
  */
244
245
  if( existingID ){
245
- let post_tbl = 'wp_posts';
246
- let post_meta_tbl = 'wp_postmeta';
247
- let term_taxonomy_tbl = 'wp_term_taxonomy';
248
- let terms_tbl = 'wp_terms';
249
-
250
- const cmd = [
251
- path.join(projectPath, 'bin', 'wp-cli.phar'),
252
- `--ssh=${request.ssh}`,
253
- '--skip-themes',
254
- `--url=${new URL(request.url).origin}`
255
- ];
256
-
257
- // if taxonomy is page/post/media.
258
- if( ['pages', 'posts', 'media'].includes(tax) ){
259
-
260
-
261
- /**
262
- * Since the REST API doesn't detected trashed posts/pages.
263
- * We have to do a drop before we can do an update.
264
- * Since trashed posts/pages stay in the database
265
- */
266
- // drop post
267
- /*await runCmd(
268
- 'php',
269
- [
270
- ...cmd,
271
- '--skip-column-names',
272
- `db query 'DELETE FROM ${post_tbl} WHERE ID=${existingID}'`,
273
- ]
274
- )
275
-
276
- // drop post meta.
277
- await runCmd(
278
- 'php',
279
- [
280
- ...cmd,
281
- '--skip-column-names',
282
- `db query 'DELETE FROM ${post_meta_tbl} WHERE post_id=${existingID}'`,
283
- ]
284
- )*/
285
-
286
- // now we can update the new ID with the existing ID.
287
- await runCmd(
288
- 'php',
289
- [
290
- ...cmd,
291
- '--skip-column-names',
292
- `db query 'UPDATE ${post_tbl} SET ID=${existingID} WHERE ID=${results.id}'`,
293
- ]
294
- )
295
- // update post meta.
296
- await runCmd(
297
- 'php',
298
- [
299
- ...cmd,
300
- '--skip-column-names',
301
- `db query 'UPDATE ${post_meta_tbl} SET post_id=${existingID} WHERE post_id=${results.id}'`,
302
- ]
303
- )
304
-
305
- // if taxonomy is menu.
306
- }else if( 'menus' === tax ){
307
- // update the terms table.
308
- await runCmd(
309
- 'php',
310
- [
311
- ...cmd,
312
- '--skip-column-names',
313
- `db query 'UPDATE ${terms_tbl} SET term_id=${existingID} WHERE term_id=${results.id}'`,
314
- ]
315
- )
316
246
 
317
- // update the term taxonomy table.
318
- await runCmd(
319
- 'php',
320
- [
321
- ...cmd,
322
- '--skip-column-names',
323
- `db query 'UPDATE ${term_taxonomy_tbl} SET term_taxonomy_id=${existingID},term_id=${existingID} WHERE term_id=${results.id}'`,
324
- ]
325
- )
326
-
327
- /**
328
- * Menus are also stored under nav_menu_locations in the the option theme_mods_<theme_name>
329
- * for each location we patch that nav_menu_locations key with the correct ID
330
- *
331
- * @todo remove hardcoding of theme name
332
- */
333
- results.locations.map( async (location) => {
334
- await runCmd(
335
- 'php',
336
- [
337
- ...cmd,
338
- 'option patch update',
339
- `theme_mods_CAWeb`,
340
- 'nav_menu_locations',
341
- location,
342
- existingID
343
- ]
344
- )
345
- })
346
-
347
- }
247
+ await axios.request({
248
+ ...request,
249
+ url: `${request.url}/wp-json/caweb-dev/v1/sync`,
250
+ timeout: 7500,
251
+ data: {
252
+ id: results.id,
253
+ newId: existingID,
254
+ tax,
255
+ locations: 'menus' === tax ? obj.locations : []
256
+ }
257
+ })
258
+ .then( async (res) => { return res.data; } )
348
259
 
349
260
  // update the API results ID, back to the existing ID from earlier.
350
261
  results.id = existingID;
@@ -3,6 +3,7 @@ import { downloadSources } from "./download-sources.js";
3
3
  import { configureDivi, isDiviThemeActive } from "./divi.js";
4
4
  import { configureWordPress, isMultisite, convertToMultisite, generateHTAccess } from "./wordpress.js";
5
5
  import { getTaxonomies, createTaxonomies } from "./api.js";
6
+ import { CAWEB_OPTIONS, DIVI_OPTIONS } from './options.js';
6
7
 
7
8
  export {
8
9
  activateCAWeb,
@@ -15,5 +16,7 @@ export {
15
16
  convertToMultisite,
16
17
  generateHTAccess,
17
18
  getTaxonomies,
18
- createTaxonomies
19
+ createTaxonomies,
20
+ CAWEB_OPTIONS,
21
+ DIVI_OPTIONS
19
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caweb/cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "CAWebPublishing Command Line Interface.",
5
5
  "exports": "./lib/env.js",
6
6
  "type": "module",