@caweb/cli 1.5.1 → 1.5.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.
@@ -3,7 +3,10 @@
3
3
  */
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
- import inquirer from 'inquirer';
6
+ // since we are in a spinner,
7
+ // we have to silence all the cancellation errors when using prompts
8
+ // .catch(() => {process.exit(1);})
9
+ import { confirm } from '@inquirer/prompts';
7
10
 
8
11
  /**
9
12
  * Internal dependencies
@@ -49,14 +52,10 @@ export default async function createBlock({
49
52
  if( fs.existsSync(path.resolve(process.cwd(), slug)) ){
50
53
  spinner.info(`${slug} already exists.`)
51
54
 
52
- const { yesUpdate } = await inquirer.prompt( [
53
- {
54
- type: 'confirm',
55
- name: 'yesUpdate',
56
- message: 'Would you like to update it?',
57
- default: false,
58
- },
59
- ] );
55
+ const yesUpdate = await confirm({
56
+ message: 'Would you like to update it?',
57
+ default: false,
58
+ } ).catch(() => {process.exit(1);});
60
59
 
61
60
  if( yesUpdate ){
62
61
  spinner.text = `Updating ${slug}...`;
package/commands/index.js CHANGED
@@ -23,7 +23,7 @@ const a11y = new A11yPlugin().a11yCheck;
23
23
 
24
24
  import shell from './tasks/shell.js';
25
25
 
26
- import sync from './sync.js';
26
+ import sync from './sync/index.js';
27
27
 
28
28
  import updatePlugins from './tasks/update-plugins.js'
29
29
  import createBlock from './blocks/create-block.js'
@@ -0,0 +1,553 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import loadConfig from '@wordpress/env/lib/config/load-config.js';
7
+ import axios from 'axios';
8
+
9
+ // since we are in a spinner,
10
+ // we have to silence all the cancellation errors when using prompts
11
+ // .catch(() => {process.exit(1);})
12
+ import { confirm, input, password } from '@inquirer/prompts';
13
+
14
+ /**
15
+ * Internal dependencies
16
+ */
17
+ import {
18
+ appPath,
19
+ getTaxonomies,
20
+ createTaxonomies
21
+ } from '../../lib/index.js';
22
+
23
+ import {
24
+ promptGetInstanceInfo,
25
+ promptSaveInstanceInfo,
26
+ promptForSync,
27
+ promptForId
28
+ } from './prompts.js';
29
+
30
+ const errors = [];
31
+ const configFile = path.join(appPath, 'caweb.json');
32
+
33
+ /**
34
+ * Return all parent items for a given set of ids.
35
+ *
36
+ * @async
37
+ * @param {Array} ids
38
+ * @param {*} request
39
+ * @param {string} [tax='pages']
40
+ * @returns {unknown}
41
+ */
42
+ async function getParentItems( objects, request, tax = 'pages' ){
43
+ let parentItemsObjects = [];
44
+ let objectParentIds = objects.map((obj) => { return obj.parent; }).filter(n => n);
45
+
46
+ while( objectParentIds.length > 0 ){
47
+
48
+ // if we have parent ids, we have to collect any parent items.
49
+ let parentItems = await getTaxonomies({
50
+ ...request,
51
+ orderby: 'parent',
52
+ embed: true,
53
+ include: objectParentIds
54
+ }, tax);
55
+
56
+ // if we have parent items, we have to add to the items array.
57
+ if( parentItems ){
58
+ parentItemsObjects = parentItemsObjects.concat( parentItems );
59
+ }
60
+
61
+ // if the parent items have parent ids, we have to save those for the next run.
62
+ objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
63
+ }
64
+
65
+ return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
66
+ }
67
+
68
+
69
+ /**
70
+ * Get information for an instance.
71
+ *
72
+ * @async
73
+ * @param {string} [instance='target']
74
+ * @returns {Object}
75
+ */
76
+ async function getInstanceInfo( config, instance = 'target'){
77
+ // ask for target information to be entered.
78
+ const inputTarget = await confirm(
79
+ {
80
+ message: `A ${instance} instance was not specified, would you like to enter the information?`,
81
+ default: true
82
+ }
83
+ ).catch(() => {process.exit(1);});
84
+
85
+ // if user said yes
86
+ if( inputTarget ){
87
+ let target = await promptGetInstanceInfo();
88
+
89
+ // ask to save information
90
+ let nickname = await promptSaveInstanceInfo()
91
+
92
+ if( nickname ){
93
+ config = config ?? {};
94
+
95
+ // add the target to sync list
96
+ config.sync[nickname] = target;
97
+
98
+ fs.writeFileSync(
99
+ configFile,
100
+ JSON.stringify(config, null, 4)
101
+ )
102
+ }
103
+
104
+ return target;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Sync Environments.
110
+ *
111
+ * @see https://developer.wordpress.org/rest-api/reference/
112
+ *
113
+ * @param {Object} options
114
+ * @param {Object} options.spinner A CLI spinner which indicates progress.
115
+ * @param {boolean} options.target Remote Site URL with current changes.
116
+ * @param {boolean} options.dest Destination Site URL that should be synced.
117
+ * @param {boolean} options.debug True if debug mode is enabled.
118
+ * @param {Array} options.tax Taxonomy that should be synced.
119
+ * @param {Array} options.include Include specific IDs only.
120
+ */
121
+ export default async function sync({
122
+ spinner,
123
+ debug,
124
+ target,
125
+ dest,
126
+ interactive,
127
+ tax,
128
+ mediaIds,
129
+ menuIds,
130
+ pageIds,
131
+ postIds
132
+ } ) {
133
+
134
+ // this gets the working directory path after wp-env validation
135
+ const {workDirectoryPath} = await loadConfig(path.resolve('.'));
136
+
137
+ // read caweb configuration file.
138
+ let serviceConfig = fs.existsSync(configFile) ? JSON.parse( fs.readFileSync(configFile) ) : {};
139
+
140
+ process.env.WP_CLI_CONFIG_PATH = path.join(workDirectoryPath, 'config.yml');
141
+
142
+ // get target and dest instance data
143
+ target = serviceConfig.sync[target];
144
+ dest = serviceConfig.sync[dest];
145
+
146
+ // if no target was specified or no target saved.
147
+ if( ! target ){
148
+ spinner.stop()
149
+
150
+ target = await getInstanceInfo(serviceConfig, 'target')
151
+
152
+ // if still no target then exit
153
+ if( ! target ){
154
+ process.exit(1)
155
+ }
156
+ }
157
+
158
+
159
+ // if no dest was specified or no dest saved.
160
+ if( ! dest ){
161
+ spinner.stop()
162
+
163
+ dest = await getInstanceInfo(serviceConfig, 'destination');
164
+
165
+
166
+ // if still no target then exit
167
+ if( ! dest ){
168
+ process.exit(1)
169
+ }
170
+ }
171
+
172
+ /**
173
+ * each instance has to have a url, user, pwd property
174
+ */
175
+ if(
176
+ ! target || ! target.url || ! target.user || ! target.pwd ||
177
+ ! dest || ! dest.url || ! dest.user || ! dest.pwd
178
+ ){
179
+ spinner.fail(`caweb.json is not configured properly for ${target} and ${dest}.`);
180
+ process.exit(1)
181
+ }
182
+
183
+ let targetOptions = {
184
+ url: target.url,
185
+ headers: {
186
+ Authorization: 'Basic ' + Buffer.from(`${target.user}:${target.pwd}`).toString('base64')
187
+ }
188
+ }
189
+ let destOptions = {
190
+ url: dest.url,
191
+ headers: {
192
+ Authorization: 'Basic ' + Buffer.from(`${dest.user}:${dest.pwd}`).toString('base64'),
193
+ 'content-type': 'multipart/form-data',
194
+ 'accept': '*/*'
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Sync Process
200
+ * If taxonomy is undefined then we don't sync that section.
201
+ *
202
+ * 1) We collect media if the taxonomy is undefined or includes media, pages, posts.
203
+ * 2) We only collect settings if the taxonomy is undefined or it's set to settings.
204
+ * 3) We only collect pages if the taxonomy is undefined or it's set to pages.
205
+ * 4) We only collect posts if the taxonomy is undefined or it's set to posts.
206
+ * 5) We only collect menus if the taxonomy is undefined or it's set to menus.
207
+ * - We also collect menu items if menus are collected.
208
+ */
209
+ let settings = [];
210
+ let mediaLibrary = [];
211
+ let media = [];
212
+ let pages = [];
213
+ let posts = [];
214
+ let menus = [];
215
+ let menuNavItems = [];
216
+
217
+ // Run prompts if interactive
218
+ if( interactive ){
219
+ spinner.stop();
220
+ tax = await promptForSync(tax);
221
+
222
+ let inputtedMediaIds = tax.includes('media') ? await promptForId('Media') : '';
223
+ let inputtedMenuIds = tax.includes('menus') ? await promptForId('Menus') : '';
224
+ let inputtedPageIds = tax.includes('pages') ? await promptForId('Pages') : '';
225
+ let inputtedPostIds = tax.includes('posts') ? await promptForId('Posts') : '';
226
+
227
+ // if inputted ids is all then we dont need ids,
228
+ inputtedMediaIds = 'all' === inputtedMediaIds ? [] : inputtedMediaIds.split(',');
229
+ inputtedMenuIds = 'all' === inputtedMenuIds ? [] : inputtedMenuIds.split(',');
230
+ inputtedPageIds = 'all' === inputtedPageIds ? [] : inputtedPageIds.split(',');
231
+ inputtedPostIds = 'all' === inputtedPostIds ? [] : inputtedPostIds.split(',');
232
+
233
+ mediaIds = mediaIds ? mediaIds.concat(inputtedMediaIds) : inputtedMediaIds;
234
+ menuIds = menuIds ? menuIds.concat(inputtedMenuIds) : inputtedMenuIds;
235
+ pageIds = pageIds ? pageIds.concat(inputtedPageIds) : inputtedPageIds;
236
+ postIds = postIds ? postIds.concat(inputtedPostIds) : inputtedPostIds;
237
+
238
+ spinner.start()
239
+
240
+ }
241
+
242
+ // Media Library.
243
+ if( tax.includes('media', 'pages', 'posts') ){
244
+ spinner.text = `Collecting Media Library ${target.url}`;
245
+ mediaLibrary = await getTaxonomies({
246
+ ...targetOptions,
247
+ fields: [
248
+ 'id',
249
+ 'source_url',
250
+ 'title',
251
+ 'caption',
252
+ 'alt_text',
253
+ 'date',
254
+ 'mime_type',
255
+ 'post',
256
+ 'media_details'
257
+ ],
258
+ include: mediaIds && mediaIds.length ? mediaIds.join(',') : null
259
+ },
260
+ 'media'
261
+ );
262
+ }
263
+
264
+ // Site Settings.
265
+ if( tax.includes('settings') ){
266
+ spinner.text = `Collecting Site Settings from ${target.url}`;
267
+ settings = await getTaxonomies({
268
+ ...targetOptions,
269
+ fields: [
270
+ 'title',
271
+ 'description',
272
+ 'show_on_front',
273
+ 'page_on_front',
274
+ 'posts_per_page'
275
+ ],
276
+ }, 'settings')
277
+
278
+ }
279
+
280
+ // Pages.
281
+ if( tax.includes('pages') ){
282
+ // get all pages/posts
283
+ spinner.text = `Collecting pages from ${target.url}`;
284
+ pages = await getTaxonomies({
285
+ ...targetOptions,
286
+ orderby: 'parent',
287
+ embed: true,
288
+ include: pageIds && pageIds.length ? pageIds.join(',') : null
289
+ },
290
+ 'pages'
291
+ );
292
+
293
+ // pages can be nested so we have to collect any parent items.
294
+ pages = await getParentItems(
295
+ pages,
296
+ targetOptions,
297
+ 'pages'
298
+ )
299
+ }
300
+
301
+ // Posts.
302
+ if( tax.includes('posts') ){
303
+ // get all pages/posts
304
+ spinner.text = `Collecting all posts from ${target.url}`;
305
+ posts = await getTaxonomies({
306
+ ...targetOptions,
307
+ orderby: 'parent',
308
+ include: postIds && postIds.length ? postIds.join(',') : null
309
+ },
310
+ 'posts'
311
+ );
312
+
313
+ // posts can be nested so we have to collect any parent items.
314
+ posts = await getParentItems(
315
+ posts,
316
+ targetOptions,
317
+ 'posts'
318
+ )
319
+ }
320
+
321
+ /**
322
+ * Media Library Handling
323
+ *
324
+ * We iterate thru the media library to identify which media should be synced.
325
+ * 1) attached media items by default are only possible on pages.
326
+ * 2) featured media by default are only possible on posts.
327
+ * 3) save any linked media in the content of pages and posts.
328
+ */
329
+ for( let m of mediaLibrary ){
330
+ // remove any new line characters.
331
+ m.source_url = m.source_url.replace('\n','');
332
+
333
+ /**
334
+ * We collect the media if:
335
+ * - media is attached
336
+ * - id was explicitly requested
337
+ * - tax includes media and media ids is blank
338
+ */
339
+ if(
340
+ m.post ||
341
+ ( mediaIds && mediaIds.includes( m.id.toString() ) ) ||
342
+ ( tax.includes('media') && undefined === mediaIds )
343
+ ){
344
+ media.push( m );
345
+
346
+ // we don't have to check any further.
347
+ continue;
348
+ }
349
+
350
+ for( let p of [].concat( pages, posts ) ){
351
+ /**
352
+ * We collect the media if:
353
+ * - media is featured image
354
+ * - media is the src attribute in the content
355
+ */
356
+ if( p.featured_media === m.id ||
357
+ p.content.rendered.match( new RegExp(`src=”(${m.source_url}.*)”`, 'g') )){
358
+ media.push( m );
359
+ }
360
+ }
361
+ }
362
+
363
+ // filter any duplicate media.
364
+ media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
365
+ let i = 0;
366
+
367
+ // before we can upload media files we have to generate the media blob data.
368
+ for( let m of media ){
369
+ if( debug ){
370
+ i++;
371
+ spinner.info(`Media ID ${m.id} Collected: ${i}/${media.length}`)
372
+ }
373
+
374
+ const mediaBlob = await axios.request(
375
+ {
376
+ ...targetOptions,
377
+ url: m.source_url,
378
+ responseType: 'arraybuffer'
379
+ }
380
+ )
381
+ .then( (img) => { return new Blob([img.data]) })
382
+ .catch( (error) => {
383
+ errors.push(`${m.source_url} could not be downloaded.`)
384
+ return false;
385
+ });
386
+
387
+ if( mediaBlob ){
388
+ m.data = mediaBlob;
389
+ }
390
+ }
391
+
392
+ // this has to be done after we have the media data.
393
+ // Lets replace the url references in the content of the pages and posts.
394
+ for( let p of [].concat( pages, posts ) ){
395
+ p.content.rendered = p.content.rendered.replace( new RegExp(target.url, 'g'), dest.url );
396
+ }
397
+
398
+
399
+ // Menu and Nav Items.
400
+ if( tax.includes('menus')){
401
+ spinner.text = `Collecting assigned navigation menus from ${target.url}`;
402
+ // get all menus and navigation links
403
+ menus = await getTaxonomies({
404
+ ...targetOptions,
405
+ fields: [
406
+ 'id',
407
+ 'description',
408
+ 'name',
409
+ 'slug',
410
+ 'meta',
411
+ 'locations'
412
+ ],
413
+ include: menuIds && menuIds.length ? menuIds.join(',') : null
414
+ }, 'menus');
415
+
416
+ menuNavItems = await getTaxonomies(
417
+ {
418
+ ...targetOptions,
419
+ fields: [
420
+ 'id',
421
+ 'title',
422
+ 'url',
423
+ 'status',
424
+ 'attr_title',
425
+ 'description',
426
+ 'type',
427
+ 'type_label',
428
+ 'object',
429
+ 'object_id',
430
+ 'parent',
431
+ 'menu_order',
432
+ 'target',
433
+ 'classes',
434
+ 'xfn',
435
+ 'meta',
436
+ 'menus'
437
+ ],
438
+ menus: menus.map((menu) => { return menu.id; })
439
+ },
440
+ 'menu-items'
441
+ )
442
+
443
+ let missingPages = [];
444
+ let missingPosts = [];
445
+ // we iterate over menu items
446
+ menuNavItems.forEach(item => {
447
+ // if the item is a page and it wasn't previously collected
448
+ if( 'page' === item.object && ! pages.map(p => p.id ).includes(item.object_id) ){
449
+ missingPages.push(item.object_id)
450
+ // if the item is a post and it wasn't previously collected
451
+ }else if( 'post' === item.object && ! posts.map(p => p.id ).includes(item.object_id) ){
452
+ missingPosts.push(item.object_id)
453
+ }
454
+ })
455
+
456
+ // if navigation pages weren't previously collected they must be collected.
457
+ if( missingPages.length ){
458
+ missingPages = await getTaxonomies({
459
+ ...targetOptions,
460
+ orderby: 'parent',
461
+ embed: true,
462
+ include: missingPages.join(',')
463
+ },
464
+ 'pages'
465
+ );
466
+
467
+ // pages can be nested so we have to collect any parent items.
468
+ missingPages = await getParentItems(
469
+ missingPages,
470
+ targetOptions,
471
+ 'pages'
472
+ );
473
+
474
+ // add the missing pages to the pages array
475
+ pages = pages.concat(missingPages)
476
+ }
477
+
478
+ // if navigation posts weren't previously collected they must be collected.
479
+ if( missingPosts.length ){
480
+ // get all pages/posts
481
+ spinner.text = `Collecting all posts from ${target.url}`;
482
+ missingPosts = await getTaxonomies({
483
+ ...targetOptions,
484
+ orderby: 'parent',
485
+ include: missingPosts.join(',')
486
+ },
487
+ 'posts'
488
+ );
489
+
490
+ // posts can be nested so we have to collect any parent items.
491
+ missingPosts = await getParentItems(
492
+ missingPosts,
493
+ targetOptions,
494
+ 'posts'
495
+ )
496
+
497
+ // add the missing posts to the posts array
498
+ posts = posts.concat(missingPages);
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Now we have all the data we can begin to create the taxonomies on the target.
504
+ *
505
+ * Import Order is important otherwise missing dependencies will cause errors to trigger
506
+ * 1) Media
507
+ * 2) Pages
508
+ * 3) Posts
509
+ * 4) Menus & Navigation Links
510
+ * 5) Settings
511
+ */
512
+
513
+ // Media.
514
+ if( media ){
515
+ spinner.text = `Uploading media files to ${dest.url}`;
516
+ await createTaxonomies(
517
+ media.filter((img) => { return img.data }),
518
+ destOptions,
519
+ 'media',
520
+ spinner
521
+ )
522
+ }
523
+
524
+ // Pages.
525
+ if( pages ){
526
+ spinner.text = `Creating all pages to ${dest.url}`;
527
+ await createTaxonomies( pages, destOptions, 'pages', spinner );
528
+ }
529
+
530
+ // Posts.
531
+ if( posts ){
532
+ spinner.text = `Creating all posts to ${dest.url}`;
533
+ await createTaxonomies( posts, destOptions, 'posts', spinner );
534
+ }
535
+
536
+ // Menus and Navigation Links.
537
+ if( menus ){
538
+ spinner.text = `Reconstructing navigation menus to ${dest.url}`;
539
+ await createTaxonomies(menus, destOptions, 'menus', spinner);
540
+ await createTaxonomies(menuNavItems, destOptions, 'menu-items', spinner);
541
+ }
542
+
543
+
544
+ // Settings.
545
+ if( settings ){
546
+ spinner.text = `Updating site settings to ${dest.url}`;
547
+ await createTaxonomies(settings, destOptions, 'settings', spinner);
548
+ }
549
+
550
+ spinner.text = `Sync from ${target.url} to ${dest.url} completed successfully.`
551
+
552
+
553
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { select } from 'inquirer-select-pro';
5
+ import chalk from 'chalk';
6
+
7
+ // since we are in a spinner,
8
+ // we have to silence all the cancellation errors when using prompts
9
+ // .catch(() => {process.exit(1);})
10
+ import { confirm, input, password } from '@inquirer/prompts';
11
+
12
+
13
+ const boldWhite = chalk.bold.white;
14
+
15
+ /**
16
+ * Prompt for instance information
17
+ * { user, pwd, url }
18
+ *
19
+ * @asyncc
20
+ * @returns {Prompt}
21
+ */
22
+ async function promptGetInstanceInfo(){
23
+ return {
24
+ user: await input({ message: "User Name:" }).catch(() => {process.exit(1);}),
25
+ pwd: await password({ message: 'Application Password:' }).catch(() => {process.exit(1);}),
26
+ url: await input({ message: 'URL:' }).catch(() => {process.exit(1);}),
27
+ };
28
+ }
29
+
30
+
31
+ /**
32
+ * Prompt for saving information
33
+ *
34
+ * @async
35
+ * @returns {Prompt}
36
+ */
37
+ async function promptSaveInstanceInfo(){
38
+ let answer = await confirm(
39
+ {
40
+ message: 'Would you like to save this information?',
41
+ default: true
42
+ }
43
+ ).catch(() => {process.exit(1);});
44
+
45
+ if( answer ){
46
+ // ask instance nickname
47
+ return await input({ message: "Instance Nickname:" }).catch(() => {process.exit(1);})
48
+ }
49
+ }
50
+
51
+ async function promptForSync(tax){
52
+ let bullet = chalk.yellow('-');
53
+ let info = chalk.cyan('i');
54
+ console.log(`Sync WordPress Instances\n${chalk.green('#'.repeat(25))}\n`);
55
+
56
+ console.log(chalk.red('Requirements:'));
57
+ console.log(bullet, 'Both instances must have the CAWebPublishing Development Toolbox WordPless plugin installed and activated');
58
+ console.log(bullet.repeat(2), 'Plugin can be found https://github.com/CAWebPublishing/caweb-dev/releases');
59
+ console.log(bullet, 'An application password must be setup for the username');
60
+ console.log(bullet.repeat(2), 'For more information regarding application password https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/');
61
+
62
+ console.log(chalk.cyan('\nNotes:'));
63
+ // syncing menus note.
64
+ console.info(info, 'If Menus is selected, the following will also be synced:');
65
+ console.info(bullet, 'Any pages/posts (including parents) in the menu items.');
66
+
67
+ // syncing pages/posts note.
68
+ console.info(info, 'If Pages/Posts is selected, the following will also be synced:');
69
+ console.info(bullet, 'Any media that is attached.');
70
+ console.info(bullet, 'Any parent pages.');
71
+
72
+ console.log('');
73
+
74
+ let taxonomies = ['Media', 'Menus', 'Pages', 'Posts', 'Settings'];
75
+ let options = [];
76
+
77
+ taxonomies.forEach(t => {
78
+ options.push({
79
+ name: t,
80
+ value: t.toLowerCase(),
81
+ checked: tax.includes(t.toLowerCase())
82
+ })
83
+ })
84
+ return await select({
85
+ message: 'Select from the following taxonomies...',
86
+ multiple: true,
87
+ canToggleAll: true,
88
+ options,
89
+ defaultValue: options.map(o => o.checked ? o.value : false ).filter( e => e)
90
+ }).catch(() => {process.exit(1);});
91
+ }
92
+
93
+ async function promptForId(title){
94
+ console.log(chalk.cyan('i'), `Enter comma separated list of IDs for ${title}`)
95
+ return await input({
96
+ message: 'Which IDs would you like to sync?',
97
+ default: 'all',
98
+ value: 'test'
99
+ }).catch(() => {process.exit(1);});
100
+ }
101
+
102
+ export {
103
+ promptGetInstanceInfo,
104
+ promptSaveInstanceInfo,
105
+ promptForSync,
106
+ promptForId
107
+ }
package/lib/cli.js CHANGED
@@ -306,15 +306,35 @@ export default function cli() {
306
306
  // Update a Design System Block Command.
307
307
  program.command('sync')
308
308
  .description('Sync changes from one WordPress instance to another.')
309
- .argument('<from>', 'Target Site URL.')
310
- .argument('<to>', 'Destination Site URL.')
309
+ .argument('[target]', 'Target Site URL.')
310
+ .argument('[dest]', 'Destination Site URL.')
311
+ .addOption(new Option(
312
+ '--interactive',
313
+ 'Runs the sync process with prompts'
314
+ ))
315
+ .addOption(
316
+ new Option(
317
+ '-t,--tax <tax...>',
318
+ 'Taxonomy that should be synced. Default is full site sync.'
319
+ )
320
+ .choices(['media', 'menus', 'pages', 'posts', 'settings'])
321
+ .default(['media', 'menus', 'pages', 'posts', 'settings'])
322
+ )
311
323
  .addOption(new Option(
312
- '-t,--tax [tax...]',
313
- 'Taxonomy that should be synced. Omitting this option will sync the full site.'
314
- ).choices(['pages', 'posts', 'media', 'menus', 'settings']))
324
+ '--media-ids <ids...>',
325
+ 'Sync specific Media IDs.'
326
+ ))
327
+ .addOption(new Option(
328
+ '--menu-ids <ids...>',
329
+ 'Sync specific Menu IDs.'
330
+ ))
331
+ .addOption(new Option(
332
+ '--page-ids <ids...>',
333
+ 'Sync specific Page IDs.'
334
+ ))
315
335
  .addOption(new Option(
316
- '-i,--include [include...]',
317
- 'IDs to of taxonomies to include. Omitting this option will sync all items for given taxonomy.'
336
+ '--post-ids <ids...>',
337
+ 'Sync specific Post IDs.'
318
338
  ))
319
339
  .allowUnknownOption(true)
320
340
  .action( withSpinner(env.sync) )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caweb/cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "CAWebPublishing Command Line Interface.",
5
5
  "exports": "./lib/env.js",
6
6
  "type": "module",
@@ -56,7 +56,8 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "@caweb/webpack": "^1.0.1",
59
+ "@caweb/webpack": "^1.0.3",
60
+ "@inquirer/prompts": "^5.2.0",
60
61
  "@wordpress/create-block": "^4.46.0",
61
62
  "@wordpress/env": "^10.3.0",
62
63
  "axios": "^1.7.2",
@@ -65,6 +66,7 @@
65
66
  "commander": "^12.1.0",
66
67
  "cross-spawn": "^7.0.3",
67
68
  "docker-compose": "^0.24.8",
69
+ "inquirer-select-pro": "^1.0.0-alpha.6",
68
70
  "ora": "^8.0.1",
69
71
  "resolve-bin": "^1.0.1",
70
72
  "terminal-link": "^3.0.0"
package/commands/sync.js DELETED
@@ -1,402 +0,0 @@
1
- /**
2
- * External dependencies
3
- */
4
- import path from 'path';
5
- import fs from 'fs';
6
- import loadConfig from '@wordpress/env/lib/config/load-config.js';
7
- import axios from 'axios';
8
-
9
- /**
10
- * Internal dependencies
11
- */
12
- import {
13
- appPath,
14
- getTaxonomies,
15
- createTaxonomies
16
- } from '../lib/index.js';
17
-
18
- const errors = [];
19
-
20
- /**
21
- * Return all parent items for a given set of ids.
22
- *
23
- * @async
24
- * @param {Array} ids
25
- * @param {*} request
26
- * @param {string} [tax='pages']
27
- * @returns {unknown}
28
- */
29
- async function getParentItems( objects, request, tax = 'pages' ){
30
- let parentItemsObjects = [];
31
- let objectParentIds = objects.map((obj) => { return obj.parent; }).filter(n => n);
32
-
33
- while( objectParentIds.length > 0 ){
34
-
35
- // if we have parent ids, we have to collect any parent items.
36
- let parentItems = await getTaxonomies({
37
- ...request,
38
- orderby: 'parent',
39
- embed: true,
40
- include: objectParentIds
41
- }, tax);
42
-
43
- // if we have parent items, we have to add to the items array.
44
- if( parentItems ){
45
- parentItemsObjects = parentItemsObjects.concat( parentItems );
46
- }
47
-
48
- // if the parent items have parent ids, we have to save those for the next run.
49
- objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
50
- }
51
-
52
- return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
53
- }
54
-
55
- /**
56
- * Sync Environments.
57
- *
58
- * @see https://developer.wordpress.org/rest-api/reference/
59
- *
60
- * @param {Object} options
61
- * @param {Object} options.spinner A CLI spinner which indicates progress.
62
- * @param {boolean} options.from Remote Site URL with current changes.
63
- * @param {boolean} options.to Destination Site URL that should be synced.
64
- * @param {boolean} options.debug True if debug mode is enabled.
65
- * @param {Array} options.tax Taxonomy that should be synced.
66
- * @param {Array} options.include Include specific IDs only.
67
- */
68
- export default async function sync({
69
- spinner,
70
- debug,
71
- from,
72
- to,
73
- tax,
74
- include
75
- } ) {
76
-
77
- const localFile = path.join(appPath, 'caweb.json');
78
- const {workDirectoryPath} = await loadConfig(path.resolve('.'));
79
-
80
- process.env.WP_CLI_CONFIG_PATH = path.join(workDirectoryPath, 'config.yml');
81
-
82
- // if caweb.json file doesn't exist we don't do anything
83
- if( ! fs.existsSync(localFile) ){
84
- spinner.fail('caweb.json file not found.')
85
- process.exit(1)
86
- }
87
-
88
- // read configuration file
89
- const serviceConfig = JSON.parse( fs.readFileSync(localFile) );
90
-
91
- /**
92
- * make sure instances are in the file.
93
- * must have sync property
94
- * each instance has to have a url, user, pwd property
95
- */
96
- if(
97
- ! serviceConfig.sync ||
98
- ! serviceConfig.sync[from] ||
99
- ! serviceConfig.sync[from].url ||
100
- ! serviceConfig.sync[from].user ||
101
- ! serviceConfig.sync[from].pwd ||
102
- ! serviceConfig.sync[to] ||
103
- ! serviceConfig.sync[to].url ||
104
- ! serviceConfig.sync[to].user ||
105
- ! serviceConfig.sync[to].pwd
106
- ){
107
- spinner.fail(`caweb.json is not configured properly for ${from} and ${to}.`);
108
- process.exit(1)
109
- }
110
-
111
- // get instance data
112
- from = serviceConfig.sync[from];
113
- let fromOptions = {
114
- url: from.url,
115
- headers: {
116
- Authorization: 'Basic ' + Buffer.from(`${from.user}:${from.pwd}`).toString('base64')
117
- }
118
- }
119
-
120
-
121
- to = serviceConfig.sync[to];
122
-
123
- let toOptions = {
124
- url: to.url,
125
- headers: {
126
- Authorization: 'Basic ' + Buffer.from(`${to.user}:${to.pwd}`).toString('base64'),
127
- 'content-type': 'multipart/form-data',
128
- 'accept': '*/*'
129
- }
130
- }
131
-
132
- /**
133
- * Sync Process
134
- * If taxonomy is undefined then we don't sync that section.
135
- *
136
- * 1) We collect media if the taxonomy is undefined or includes media, pages, posts.
137
- * 2) We only collect settings if the taxonomy is undefined or it's set to settings.
138
- * 3) We only collect pages if the taxonomy is undefined or it's set to pages.
139
- * 4) We only collect posts if the taxonomy is undefined or it's set to posts.
140
- * 5) We only collect menus if the taxonomy is undefined or it's set to menus.
141
- * - We also collect menu items if menus are collected.
142
- */
143
- let settings = [];
144
- let mediaLibrary = [];
145
- let media = [];
146
- let pages = [];
147
- let posts = [];
148
- let menus = [];
149
- let menuNavItems = [];
150
-
151
- // Media Library.
152
- if( undefined === tax || tax.includes('media', 'pages', 'posts') ){
153
- spinner.text = `Collecting Media Library ${from.url}`;
154
- mediaLibrary = await getTaxonomies({
155
- ...fromOptions,
156
- fields: [
157
- 'id',
158
- 'source_url',
159
- 'title',
160
- 'caption',
161
- 'alt_text',
162
- 'date',
163
- 'mime_type',
164
- 'post',
165
- 'media_details'
166
- ],
167
- include: tax && tax.includes('media') && include ? include.join(',') : null
168
- },
169
- 'media'
170
- );
171
- }
172
-
173
- // Site Settings.
174
- if( undefined === tax || tax.includes('settings') ){
175
- spinner.text = `Collecting Site Settings from ${from.url}`;
176
- settings = await getTaxonomies({
177
- ...fromOptions,
178
- fields: [
179
- 'title',
180
- 'description',
181
- 'show_on_front',
182
- 'page_on_front',
183
- 'posts_per_page'
184
- ],
185
- }, 'settings')
186
-
187
- }
188
-
189
- // Pages.
190
- if( undefined === tax || tax.includes('pages') ){
191
- // get all pages/posts
192
- spinner.text = `Collecting pages from ${from.url}`;
193
- pages = await getTaxonomies({
194
- ...fromOptions,
195
- orderby: 'parent',
196
- embed: true,
197
- include: tax && include ? include.join(',') : null
198
- },
199
- 'pages'
200
- );
201
-
202
- // we only do this if specific ids were requested.
203
- if( include ){
204
- // if we have parent ids, we have to collect any parent items.
205
- pages = await getParentItems(
206
- pages,
207
- fromOptions,
208
- 'pages'
209
- )
210
- }
211
- }
212
-
213
- // Posts.
214
- if( undefined === tax || tax.includes('posts') ){
215
- // get all pages/posts
216
- spinner.text = `Collecting all posts from ${from.url}`;
217
- posts = await getTaxonomies({
218
- ...fromOptions,
219
- orderby: 'parent',
220
- include: tax && include ? include.join(',') : null
221
- },
222
- 'posts'
223
- );
224
-
225
- // we only do this if specific ids were requested.
226
- if( include ){
227
- // if we have parent ids, we have to collect any parent items.
228
- posts = await getParentItems(
229
- posts,
230
- fromOptions,
231
- 'posts'
232
- )
233
- }
234
- }
235
-
236
- /**
237
- * Media Library Handling
238
- *
239
- * We iterate thru the media library to identify which media should be synced.
240
- * 1) attached media items by default are only possible on pages.
241
- * 2) featured media by default are only possible on posts.
242
- * 3) save any linked media in the content of pages and posts.
243
- */
244
- for( let m of mediaLibrary ){
245
- // remove any new line characters.
246
- m.source_url = m.source_url.replace('\n','');
247
-
248
- // check if the media is attached to a page.
249
- // if tax is undefined, we collect all media attached to posts and pages.
250
- // if tax is defined, if include is undefined or media is matches post/page, we collect the media.
251
- if( ( ! tax && m.post ) ||
252
- ( tax &&
253
- ( undefined === include || include.includes( m.id.toString() ) )
254
- ) ){
255
- media.push( m );
256
-
257
- // we don't have to check any further.
258
- continue;
259
- }
260
-
261
- for( let p of [].concat( pages, posts ) ){
262
- // if the media is featured on a post or linked in the content.
263
- if( p.featured_media === m.id ||
264
- p.content.rendered.match( new RegExp(`src=&#8221;(${m.source_url}.*)&#8221;`, 'g') )){
265
- media.push( m );
266
- }
267
- }
268
- }
269
-
270
- // filter any duplicate media.
271
- media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
272
- let i = 0;
273
-
274
- // before we can upload media files we have to generate the media blob data.
275
- for( let m of media ){
276
- if( debug ){
277
- i++;
278
- spinner.info(`Media ID ${m.id} Collected: ${i}/${media.length}`)
279
- }
280
-
281
- const mediaBlob = await axios.request(
282
- {
283
- ...fromOptions,
284
- url: m.source_url,
285
- responseType: 'arraybuffer'
286
- }
287
- )
288
- .then( (img) => { return new Blob([img.data]) })
289
- .catch( (error) => {
290
- errors.push(`${m.source_url} could not be downloaded.`)
291
- return false;
292
- });
293
-
294
- if( mediaBlob ){
295
- m.data = mediaBlob;
296
- }
297
- }
298
-
299
- // this has to be done after we have the media data.
300
- // Lets replace the url references in the content of the pages and posts.
301
- for( let p of [].concat( pages, posts ) ){
302
- p.content.rendered = p.content.rendered.replace( new RegExp(from.url, 'g'), to.url );
303
- }
304
-
305
-
306
- // Menu and Nav Items.
307
- if( undefined === tax || tax.includes('menus')){
308
- spinner.text = `Collecting assigned navigation menus from ${from.url}`;
309
- // get all menus and navigation links
310
- menus = await getTaxonomies({
311
- ...fromOptions,
312
- fields: [
313
- 'id',
314
- 'description',
315
- 'name',
316
- 'slug',
317
- 'meta',
318
- 'locations'
319
- ],
320
- include: tax && include ? include.join(',') : null
321
- }, 'menus');
322
-
323
- menuNavItems = await getTaxonomies(
324
- {
325
- ...fromOptions,
326
- fields: [
327
- 'id',
328
- 'title',
329
- 'url',
330
- 'status',
331
- 'attr_title',
332
- 'description',
333
- 'type',
334
- 'type_label',
335
- 'object',
336
- 'object_id',
337
- 'parent',
338
- 'menu_order',
339
- 'target',
340
- 'classes',
341
- 'xfn',
342
- 'meta',
343
- 'menus'
344
- ],
345
- menus: menus.map((menu) => { return menu.id; })
346
- },
347
- 'menu-items'
348
- )
349
-
350
- }
351
-
352
- /**
353
- * Now we have all the data we can begin to create the taxonomies on the target.
354
- *
355
- * Import Order is important otherwise missing dependencies will cause errors to trigger
356
- * 1) Media
357
- * 2) Pages
358
- * 3) Posts
359
- * 4) Menus & Navigation Links
360
- * 5) Settings
361
- */
362
-
363
- // Media.
364
- if( media ){
365
- spinner.text = `Uploading media files to ${to.url}`;
366
- await createTaxonomies(
367
- media.filter((img) => { return img.data }),
368
- toOptions,
369
- 'media',
370
- spinner
371
- )
372
- }
373
-
374
- // Pages.
375
- if( pages ){
376
- spinner.text = `Creating all pages to ${to.url}`;
377
- await createTaxonomies( pages, toOptions, 'pages', spinner );
378
- }
379
-
380
- // Posts.
381
- if( posts ){
382
- spinner.text = `Creating all posts to ${to.url}`;
383
- await createTaxonomies( posts, toOptions, 'posts', spinner );
384
- }
385
-
386
- // Menus and Navigation Links.
387
- if( menus ){
388
- spinner.text = `Reconstructing navigation menus to ${to.url}`;
389
- await createTaxonomies(menus, toOptions, 'menus', spinner);
390
- await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
391
- }
392
-
393
-
394
- // Settings.
395
- if( settings ){
396
- spinner.text = `Updating site settings to ${to.url}`;
397
- await createTaxonomies(settings, toOptions, 'settings', spinner);
398
- }
399
-
400
- spinner.text = `Sync from ${from.url} to ${to.url} completed successfully.`
401
-
402
- };