@caweb/cli 1.3.2 → 1.3.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/README.md +20 -11
- package/commands/sync.js +203 -92
- package/configs/docker-compose.js +0 -15
- package/lib/cli.js +5 -1
- package/lib/wordpress/api.js +98 -51
- package/package.json +2 -1
- package/bin/wp-cli.phar +0 -0
package/README.md
CHANGED
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
`caweb-cli` is a tool which rapidly sets up a local WordPress environment fully configured for the [CAWebPublishing Service](https://caweb.cdt.ca.gov/), allows for the creation of Gutenberg blocks with the CAWebPublishing template configurations, and much more. The cli will automatically generate the necessary [.wp-env.json](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-json) file, to override or add additional configuration options use the [.wp-env.override.json](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-override-json) file.
|
|
3
3
|
|
|
4
4
|
*`caweb-cli` is largely inspired by WordPress Packages major thanks to the whole WordPress team and community!*
|
|
5
|
-
The following WordPress packages are used:
|
|
6
|
-
[wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/)
|
|
7
|
-
[create-block](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/)
|
|
8
5
|
|
|
9
6
|
## Prerequisites
|
|
10
7
|
- Latest version of [Docker Desktop](https://www.docker.com/products/docker-desktop), which includes [Compose v2](https://docs.docker.com/compose/migrate/), is installed.
|
|
11
8
|
- <strong>For Debian-Based Linux distributions:</strong> <code>docker-compose</code> may need to be installed with: <code>sudo apt-get install docker-compose</code>.
|
|
12
9
|
- <strong>For Windows users:</strong> [WSL should be set to version 2 for Windows Docker Desktop compatibility](https://docs.docker.com/desktop/windows/wsl/).
|
|
13
10
|
- git is installed.
|
|
14
|
-
- php is installed.
|
|
15
11
|
|
|
16
12
|
## Additional Features
|
|
17
13
|
- Downloads and configures the [CAWeb Theme](https://github.com/CAWebPublishing/CAWeb)
|
|
@@ -21,15 +17,13 @@ The following WordPress packages are used:
|
|
|
21
17
|
- phpMyAdmin development site starts at http://localhost:8080
|
|
22
18
|
- phpMyAdmin test site started at http://localhost:9090
|
|
23
19
|
- Uses CAWebPublishing [External Project Template Configuration](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/packages-create-block-external-template/) when creating Gutenberg Blocks, see configurations [here](https://github.com/CAWebPublishing/cli/lib/template)
|
|
24
|
-
- Allows for syncing of WordPress instance via Rest API
|
|
25
|
-
- Adds config.yml to both cli containers
|
|
20
|
+
- Allows for syncing of WordPress instance via Rest API, to maintain ID's please ensure [CAWebPublishing Development Toolbox](https://github.com/CAWebPublishing/caweb-dev/) plugin is installed.
|
|
21
|
+
- Adds config.yml to both cli containers.
|
|
26
22
|
<b>Example config.yml file</b>
|
|
27
23
|
<pre>
|
|
28
24
|
path: /var/www/html
|
|
29
25
|
apache_modules:
|
|
30
26
|
- mod_rewrite
|
|
31
|
-
@staging:
|
|
32
|
-
ssh: wpcli@staging.wp-cli.org
|
|
33
27
|
</pre>
|
|
34
28
|
|
|
35
29
|
|
|
@@ -81,7 +75,7 @@ Options:
|
|
|
81
75
|
</pre>
|
|
82
76
|
### `caweb run <container> [command...]`
|
|
83
77
|
<pre>
|
|
84
|
-
caweb run
|
|
78
|
+
caweb run <container> [command...]
|
|
85
79
|
|
|
86
80
|
Runs an arbitrary command in one of the underlying Docker containers. A double
|
|
87
81
|
dash can be used to pass arguments to the container without parsing them. This
|
|
@@ -166,7 +160,7 @@ Options:
|
|
|
166
160
|
|
|
167
161
|
### `caweb create-block [options] <slug>`
|
|
168
162
|
<pre>
|
|
169
|
-
caweb create-block [options]
|
|
163
|
+
caweb create-block [options] <slug>
|
|
170
164
|
|
|
171
165
|
Scaffold for WordPress plugin to register CA.gov Design System Block.
|
|
172
166
|
|
|
@@ -178,7 +172,7 @@ Options:
|
|
|
178
172
|
</pre>
|
|
179
173
|
### `caweb update-block [options] <slug>`
|
|
180
174
|
<pre>
|
|
181
|
-
caweb update-block [options]
|
|
175
|
+
caweb update-block [options] <slug>
|
|
182
176
|
|
|
183
177
|
Updates a CA.gov Design System Block.
|
|
184
178
|
|
|
@@ -188,3 +182,18 @@ Arguments:
|
|
|
188
182
|
Options:
|
|
189
183
|
-h, --help display help for command
|
|
190
184
|
</pre>
|
|
185
|
+
### `caweb sync <from> <to>`
|
|
186
|
+
In order for the sync process to work correctly, you must have a caweb.json file in the project root directory. For more information read [caweb.json](./docs/SYNC.MD) configuration.
|
|
187
|
+
<pre>
|
|
188
|
+
caweb sync <from> <to>
|
|
189
|
+
|
|
190
|
+
Sync changes from one destination to another.
|
|
191
|
+
|
|
192
|
+
Arguments:
|
|
193
|
+
from Target Site URL with current changes.
|
|
194
|
+
to Destination Site URL that should be synced.
|
|
195
|
+
|
|
196
|
+
Options:
|
|
197
|
+
-h, --help display help for command
|
|
198
|
+
-t,--tax [tax...] Taxonomy that should be synced. Omitting this option will sync the full site. (choices: "pages", "posts", "media", "menus")
|
|
199
|
+
</pre>
|
package/commands/sync.js
CHANGED
|
@@ -14,26 +14,65 @@ import {
|
|
|
14
14
|
getTaxonomies,
|
|
15
15
|
createTaxonomies
|
|
16
16
|
} from '../lib/index.js';
|
|
17
|
+
import { get } from 'http';
|
|
17
18
|
|
|
18
19
|
const errors = [];
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Return all parent items for a given set of ids.
|
|
23
|
+
*
|
|
24
|
+
* @async
|
|
25
|
+
* @param {Array} ids
|
|
26
|
+
* @param {*} request
|
|
27
|
+
* @param {string} [tax='pages']
|
|
28
|
+
* @returns {unknown}
|
|
29
|
+
*/
|
|
30
|
+
async function getParentItems( objects, request, tax = 'pages' ){
|
|
31
|
+
let parentItemsObjects = [];
|
|
32
|
+
let objectParentIds = objects.map((obj) => { return obj.parent; });
|
|
33
|
+
|
|
34
|
+
while( objectParentIds.length > 0 ){
|
|
35
|
+
|
|
36
|
+
// if we have parent ids, we have to collect any parent items.
|
|
37
|
+
let parentItems = await getTaxonomies({
|
|
38
|
+
...request,
|
|
39
|
+
orderby: 'parent',
|
|
40
|
+
embed: true,
|
|
41
|
+
include: objectParentIds
|
|
42
|
+
}, tax);
|
|
43
|
+
|
|
44
|
+
// if we have parent items, we have to add to the items array.
|
|
45
|
+
if( parentItems ){
|
|
46
|
+
parentItemsObjects = parentItemsObjects.concat( parentItems );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// if the parent items have parent ids, we have to save those for the next run.
|
|
50
|
+
objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
|
|
54
|
+
}
|
|
20
55
|
|
|
21
56
|
/**
|
|
22
57
|
* Sync Environments.
|
|
23
58
|
*
|
|
59
|
+
* @see https://developer.wordpress.org/rest-api/reference/
|
|
60
|
+
*
|
|
24
61
|
* @param {Object} options
|
|
25
62
|
* @param {Object} options.spinner A CLI spinner which indicates progress.
|
|
26
63
|
* @param {boolean} options.from Remote Site URL with current changes.
|
|
27
64
|
* @param {boolean} options.to Destination Site URL that should be synced.
|
|
28
65
|
* @param {boolean} options.debug True if debug mode is enabled.
|
|
29
66
|
* @param {Array} options.tax Taxonomy that should be synced.
|
|
67
|
+
* @param {Array} options.include Include specific IDs only.
|
|
30
68
|
*/
|
|
31
69
|
export default async function sync({
|
|
32
70
|
spinner,
|
|
33
71
|
debug,
|
|
34
72
|
from,
|
|
35
73
|
to,
|
|
36
|
-
tax
|
|
74
|
+
tax,
|
|
75
|
+
include
|
|
37
76
|
} ) {
|
|
38
77
|
|
|
39
78
|
const localFile = path.join(appPath, 'caweb.json');
|
|
@@ -79,13 +118,11 @@ export default async function sync({
|
|
|
79
118
|
}
|
|
80
119
|
}
|
|
81
120
|
|
|
82
|
-
let ssh = 'local' !== to ? to : `docker:${path.basename(workDirectoryPath)}-cli-1`;
|
|
83
121
|
|
|
84
122
|
to = serviceConfig.sync[to];
|
|
85
123
|
|
|
86
124
|
let toOptions = {
|
|
87
125
|
url: to.url,
|
|
88
|
-
ssh,
|
|
89
126
|
headers: {
|
|
90
127
|
Authorization: 'Basic ' + Buffer.from(`${to.user}:${to.pwd}`).toString('base64'),
|
|
91
128
|
'content-type': 'multipart/form-data',
|
|
@@ -93,100 +130,165 @@ export default async function sync({
|
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Sync Process
|
|
135
|
+
* If taxonomy is undefined then we don't sync that section.
|
|
136
|
+
*
|
|
137
|
+
* 1) Site Settings are always synced.
|
|
138
|
+
* 2) We always collect all the media.
|
|
139
|
+
* 3) We only collect pages if the taxonomy is undefined or it's set to pages.
|
|
140
|
+
* 4) We only collect posts if the taxonomy is undefined or it's set to posts.
|
|
141
|
+
* 5) We only collect menus if the taxonomy is undefined or it's set to menus.
|
|
142
|
+
* - We also collect menu items if menus are collected.
|
|
143
|
+
*/
|
|
144
|
+
let settings = [];
|
|
145
|
+
let media = [];
|
|
96
146
|
let pages = [];
|
|
97
147
|
let posts = [];
|
|
98
|
-
let attachedMedia = [];
|
|
99
|
-
let featuredMedia = [];
|
|
100
148
|
let menus = [];
|
|
101
149
|
let menuNavItems = [];
|
|
102
150
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
spinner.text = `Gathering all pages from ${from.url}`;
|
|
123
|
-
pages = await getTaxonomies(fromOptions);
|
|
124
|
-
}
|
|
151
|
+
// Media Library.
|
|
152
|
+
spinner.text = `Collecting Media Library ${from.url}`;
|
|
153
|
+
let mediaLibrary = await getTaxonomies({
|
|
154
|
+
...fromOptions,
|
|
155
|
+
fields: [
|
|
156
|
+
'id',
|
|
157
|
+
'source_url',
|
|
158
|
+
'title',
|
|
159
|
+
'caption',
|
|
160
|
+
'alt_text',
|
|
161
|
+
'date',
|
|
162
|
+
'mime_type',
|
|
163
|
+
'post',
|
|
164
|
+
'media_details'
|
|
165
|
+
],
|
|
166
|
+
include: tax.includes('media') && include ? include.join(',') : null
|
|
167
|
+
},
|
|
168
|
+
'media'
|
|
169
|
+
);
|
|
125
170
|
|
|
126
|
-
//
|
|
127
|
-
if( undefined === tax || tax.includes('
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
// Site Settings.
|
|
172
|
+
if( undefined === tax || tax.includes('settings') ){
|
|
173
|
+
spinner.text = `Collecting Site Settings from ${from.url}`;
|
|
174
|
+
settings = await getTaxonomies({
|
|
175
|
+
...fromOptions,
|
|
176
|
+
fields: [
|
|
177
|
+
'show_on_front',
|
|
178
|
+
'page_on_front',
|
|
179
|
+
'posts_per_page'
|
|
180
|
+
],
|
|
181
|
+
}, 'settings')
|
|
182
|
+
|
|
131
183
|
}
|
|
132
184
|
|
|
133
|
-
//
|
|
134
|
-
if( undefined === tax || tax.includes('
|
|
135
|
-
|
|
136
|
-
spinner.text = `Collecting
|
|
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({
|
|
185
|
+
// Pages.
|
|
186
|
+
if( undefined === tax || tax.includes('pages') ){
|
|
187
|
+
// get all pages/posts
|
|
188
|
+
spinner.text = `Collecting pages from ${from.url}`;
|
|
189
|
+
pages = await getTaxonomies({
|
|
152
190
|
...fromOptions,
|
|
153
|
-
|
|
154
|
-
|
|
191
|
+
orderby: 'parent',
|
|
192
|
+
embed: true,
|
|
193
|
+
include: tax && include ? include.join(',') : null
|
|
155
194
|
},
|
|
156
|
-
'
|
|
195
|
+
'pages'
|
|
157
196
|
);
|
|
158
197
|
|
|
159
|
-
|
|
198
|
+
// we only do this if specific ids were requested.
|
|
199
|
+
if( include ){
|
|
200
|
+
// if we have parent ids, we have to collect any parent items.
|
|
201
|
+
pages = await getParentItems(
|
|
202
|
+
pages,
|
|
203
|
+
fromOptions,
|
|
204
|
+
'pages'
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Posts.
|
|
210
|
+
if( undefined === tax || tax.includes('posts') ){
|
|
211
|
+
// get all pages/posts
|
|
212
|
+
spinner.text = `Collecting all posts from ${from.url}`;
|
|
213
|
+
posts = await getTaxonomies({
|
|
160
214
|
...fromOptions,
|
|
161
|
-
|
|
162
|
-
include:
|
|
215
|
+
orderby: 'parent',
|
|
216
|
+
include: tax && include ? include.join(',') : null
|
|
163
217
|
},
|
|
164
|
-
'
|
|
218
|
+
'posts'
|
|
165
219
|
);
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
220
|
+
|
|
221
|
+
// we only do this if specific ids were requested.
|
|
222
|
+
if( include ){
|
|
223
|
+
// if we have parent ids, we have to collect any parent items.
|
|
224
|
+
posts = await getParentItems(
|
|
225
|
+
posts,
|
|
226
|
+
fromOptions,
|
|
227
|
+
'posts'
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Media Library Handling
|
|
234
|
+
*
|
|
235
|
+
* We iterate thru the media library to identify which media should be synced.
|
|
236
|
+
* 1) attached media items by default are only possible on pages.
|
|
237
|
+
* 2) featured media by default are only possible on posts.
|
|
238
|
+
* 3) save any linked media in the content of pages and posts.
|
|
239
|
+
*/
|
|
240
|
+
for( let m of mediaLibrary ){
|
|
241
|
+
// check if the media is attached to a page.
|
|
242
|
+
// if tax is undefined, we collect all media attached to posts and pages.
|
|
243
|
+
// if tax is defined, we only collect media attached to posts and pages if the id match.
|
|
244
|
+
if( ( ! tax && m.post ) ||
|
|
245
|
+
( tax && include.includes(m.id.toString()) ) ){
|
|
246
|
+
media.push( m );
|
|
247
|
+
|
|
248
|
+
// we don't have to check any further.
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for( let p of [].concat( pages, posts ) ){
|
|
253
|
+
// if the media is featured on a post or linked in the content.
|
|
254
|
+
if( p.featured_media === m.id ||
|
|
255
|
+
p.content.rendered.match( new RegExp(`src=”(${m.source_url}.*)”`, 'g') )){
|
|
256
|
+
media.push( m );
|
|
185
257
|
}
|
|
186
258
|
}
|
|
187
259
|
}
|
|
260
|
+
|
|
261
|
+
// filter any duplicate media.
|
|
262
|
+
media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
|
|
263
|
+
|
|
264
|
+
// before we can upload media files we have to generate the media blob data.
|
|
265
|
+
for( let m of media ){
|
|
266
|
+
const mediaBlob = await axios.request(
|
|
267
|
+
{
|
|
268
|
+
...fromOptions,
|
|
269
|
+
url: m.source_url,
|
|
270
|
+
responseType: 'arraybuffer'
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
.then( (img) => { return new Blob([img.data]) })
|
|
274
|
+
.catch( (error) => {
|
|
275
|
+
errors.push(`${m.source_url} could not be downloaded.`)
|
|
276
|
+
return false;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if( mediaBlob ){
|
|
280
|
+
m.data = mediaBlob;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// this has to be done after we have the media data.
|
|
285
|
+
// Lets replace the url references in the content of the pages and posts.
|
|
286
|
+
for( let p of [].concat( pages, posts ) ){
|
|
287
|
+
p.content.rendered = p.content.rendered.replace( new RegExp(from.url, 'g'), to.url );
|
|
288
|
+
}
|
|
188
289
|
|
|
189
|
-
|
|
290
|
+
|
|
291
|
+
// Menu and Nav Items.
|
|
190
292
|
if( undefined === tax || tax.includes('menus')){
|
|
191
293
|
spinner.text = `Collecting assigned navigation menus from ${from.url}`;
|
|
192
294
|
// get all menus and navigation links
|
|
@@ -199,7 +301,8 @@ export default async function sync({
|
|
|
199
301
|
'slug',
|
|
200
302
|
'meta',
|
|
201
303
|
'locations'
|
|
202
|
-
]
|
|
304
|
+
],
|
|
305
|
+
include: tax && include ? include.join(',') : null
|
|
203
306
|
}, 'menus');
|
|
204
307
|
|
|
205
308
|
menuNavItems = await getTaxonomies(
|
|
@@ -224,6 +327,7 @@ export default async function sync({
|
|
|
224
327
|
'meta',
|
|
225
328
|
'menus'
|
|
226
329
|
],
|
|
330
|
+
include: tax && include ? include.join(',') : null,
|
|
227
331
|
menus: menus.map((menu) => { return menu.id; })
|
|
228
332
|
},
|
|
229
333
|
'menu-items'
|
|
@@ -234,43 +338,50 @@ export default async function sync({
|
|
|
234
338
|
/**
|
|
235
339
|
* Now we have all the data we can begin to create the taxonomies on the target.
|
|
236
340
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
341
|
+
* Import Order is important otherwise missing dependencies will cause errors to trigger
|
|
342
|
+
* 1) Media
|
|
343
|
+
* 2) Pages
|
|
344
|
+
* 3) Posts
|
|
345
|
+
* 4) Menus & Navigation Links
|
|
346
|
+
* 5) Settings
|
|
239
347
|
*/
|
|
240
348
|
|
|
241
|
-
//
|
|
242
|
-
if(
|
|
349
|
+
// Media.
|
|
350
|
+
if( media ){
|
|
243
351
|
spinner.text = `Uploading media files to ${to.url}`;
|
|
244
352
|
await createTaxonomies(
|
|
245
|
-
|
|
353
|
+
media.filter((img) => { return img.data }),
|
|
246
354
|
toOptions,
|
|
247
355
|
'media',
|
|
248
356
|
spinner
|
|
249
357
|
)
|
|
250
358
|
}
|
|
251
359
|
|
|
252
|
-
//
|
|
253
|
-
if(
|
|
360
|
+
// Pages.
|
|
361
|
+
if( pages ){
|
|
254
362
|
spinner.text = `Creating all pages to ${to.url}`;
|
|
255
363
|
await createTaxonomies( pages, toOptions, 'pages', spinner );
|
|
256
364
|
}
|
|
257
365
|
|
|
258
|
-
//
|
|
259
|
-
if(
|
|
366
|
+
// Posts.
|
|
367
|
+
if( posts ){
|
|
260
368
|
spinner.text = `Creating all posts to ${to.url}`;
|
|
261
369
|
await createTaxonomies( posts, toOptions, 'posts', spinner );
|
|
262
370
|
}
|
|
263
371
|
|
|
264
|
-
//
|
|
265
|
-
if(
|
|
372
|
+
// Menus and Navigation Links.
|
|
373
|
+
if( menus ){
|
|
266
374
|
spinner.text = `Reconstructing navigation menus to ${to.url}`;
|
|
267
375
|
await createTaxonomies(menus, toOptions, 'menus', spinner);
|
|
268
376
|
await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
|
|
269
377
|
}
|
|
270
378
|
|
|
271
379
|
|
|
272
|
-
//
|
|
273
|
-
|
|
380
|
+
// Settings.
|
|
381
|
+
if( settings ){
|
|
382
|
+
spinner.text = `Updating site settings to ${to.url}`;
|
|
383
|
+
await createTaxonomies(settings, toOptions, 'settings', spinner);
|
|
384
|
+
}
|
|
274
385
|
|
|
275
386
|
spinner.text = `Sync from ${from.url} to ${to.url} completed successfully.`
|
|
276
387
|
|
|
@@ -24,21 +24,6 @@ async function generateCLIConfig(workDirectoryPath){
|
|
|
24
24
|
apache_modules: ['mod_rewrite']
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
let { homedir } = os.userInfo();
|
|
28
|
-
|
|
29
|
-
// add any ssh hosts to the config
|
|
30
|
-
if( fs.existsSync( path.join( homedir, '.ssh', 'config' )) ){
|
|
31
|
-
|
|
32
|
-
let ssh_hosts = fs.readFileSync(path.join( homedir, '.ssh', 'config' )).toString().match(/Host\s+(.*)[^\n\r]/g);
|
|
33
|
-
|
|
34
|
-
ssh_hosts.forEach((host) => {
|
|
35
|
-
let s = host.replace(/Host\s+/, '');
|
|
36
|
-
|
|
37
|
-
yml[`@${s}`] = { ssh: s};
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
27
|
fs.writeFileSync(
|
|
43
28
|
path.join(workDirectoryPath, 'config.yml'),
|
|
44
29
|
yaml.dump(yml));
|
package/lib/cli.js
CHANGED
|
@@ -262,7 +262,11 @@ export default function cli() {
|
|
|
262
262
|
.addOption(new Option(
|
|
263
263
|
'-t,--tax [tax...]',
|
|
264
264
|
'Taxonomy that should be synced. Omitting this option will sync the full site.'
|
|
265
|
-
).choices(['pages', 'posts', 'media', 'menus']))
|
|
265
|
+
).choices(['pages', 'posts', 'media', 'menus', 'settings']))
|
|
266
|
+
.addOption(new Option(
|
|
267
|
+
'-i,--include [include...]',
|
|
268
|
+
'IDs to of taxonomies to include. Omitting this option will sync all items for given taxonomy.'
|
|
269
|
+
))
|
|
266
270
|
.allowUnknownOption(true)
|
|
267
271
|
.action( withSpinner(env.sync) )
|
|
268
272
|
|
package/lib/wordpress/api.js
CHANGED
|
@@ -4,21 +4,29 @@
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import terminalLink from 'terminal-link';
|
|
7
|
-
|
|
7
|
+
import axiosRetry from 'axios-retry';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
projectPath,
|
|
14
|
-
runCmd
|
|
15
|
-
} from '../index.js';
|
|
16
|
-
|
|
17
12
|
const endpoint = '/wp-json/wp/v2';
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const syncEndpoint = '/wp-json/caweb/v1/sync';
|
|
14
|
+
|
|
15
|
+
// Axios Configurations.
|
|
16
|
+
axiosRetry(axios,
|
|
17
|
+
{
|
|
18
|
+
retries: 3, // attempt requests 3 times
|
|
19
|
+
shouldResetTimeout: true, // reset timeout on retries
|
|
20
|
+
retryCondition: (error) => {
|
|
21
|
+
if( error.response && error.response.status === 500 ){
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}, // retry on any network or 5xx error
|
|
25
|
+
retryDelay: (retryCount, error ) => {
|
|
26
|
+
return 5000 // retry delay
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
22
30
|
|
|
23
31
|
function processData( data ){
|
|
24
32
|
/**
|
|
@@ -73,19 +81,19 @@ async function getTaxonomies( request, tax = 'pages' ){
|
|
|
73
81
|
'categories',
|
|
74
82
|
'tags',
|
|
75
83
|
'meta',
|
|
84
|
+
'parent',
|
|
85
|
+
'_links'
|
|
76
86
|
];
|
|
77
87
|
|
|
78
88
|
let urlParams = [
|
|
79
|
-
'order=asc'
|
|
89
|
+
'order=' + ( request.order ? processUrlParam(request.order) : 'asc' )
|
|
80
90
|
]
|
|
81
91
|
|
|
82
|
-
|
|
83
92
|
// if fields are passed
|
|
84
93
|
if( fields.length && 'all' !== fields ){
|
|
85
94
|
urlParams.push( `_fields=${ fields.join(',')}` );
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
|
|
89
97
|
// if no request is added default to GET
|
|
90
98
|
if( ! request.method ){
|
|
91
99
|
request.method = 'GET'
|
|
@@ -106,13 +114,23 @@ async function getTaxonomies( request, tax = 'pages' ){
|
|
|
106
114
|
urlParams.push( 'menus=' + processUrlParam( request.menus ) );
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
// if
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// if taxonomies is not menus, we add the orderby parameter.
|
|
118
|
+
if( 'menus' !== tax ){
|
|
119
|
+
urlParams.push( `orderby=` + (request.orderby ? processUrlParam(request.orderby) : 'id') );
|
|
120
|
+
}
|
|
112
121
|
|
|
113
|
-
// if
|
|
114
|
-
|
|
122
|
+
// if embed is set to true, we add the _embed parameter.
|
|
123
|
+
if( request.embed ){
|
|
124
|
+
urlParams.push( '_embed' );
|
|
125
|
+
}
|
|
115
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The per_page parameter is capped at 100.
|
|
129
|
+
*
|
|
130
|
+
* @see https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
|
|
131
|
+
*/
|
|
132
|
+
request.per_page = request.per_page && request.per_page <= 100 ? request.per_page : 100;
|
|
133
|
+
urlParams.push( 'per_page=' + request.per_page );
|
|
116
134
|
|
|
117
135
|
let collection = [];
|
|
118
136
|
let results = false;
|
|
@@ -172,8 +190,26 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
|
|
|
172
190
|
for( let obj of taxData ){
|
|
173
191
|
// endpoint url.
|
|
174
192
|
let url = `${request.url}${endpoint}/${tax}`;
|
|
175
|
-
let existingID = false;
|
|
193
|
+
let existingID = obj.id ? obj.id : false;
|
|
176
194
|
|
|
195
|
+
// process object properties.
|
|
196
|
+
for( let prop in obj ){
|
|
197
|
+
// we process the rendered property and delete the rendered property.
|
|
198
|
+
if( 'object' === typeof obj[prop] && null !== obj[prop] && obj[prop].hasOwnProperty('rendered') ){
|
|
199
|
+
obj[prop] = processData(obj[prop].rendered);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// if obj has a parent and it's 0, we delete it.
|
|
203
|
+
if( 'parent' === prop && 0 === obj[prop] ){
|
|
204
|
+
delete obj[prop];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// disallowed props.
|
|
208
|
+
if( ['_links', '_embedded'].includes(prop) ){
|
|
209
|
+
delete obj[prop];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
177
213
|
// 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
214
|
if( obj.id ){
|
|
179
215
|
// first we check if the ID exist
|
|
@@ -187,9 +223,11 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
|
|
|
187
223
|
.then((res) => { return res.data.id })
|
|
188
224
|
.catch(error => {return false;})
|
|
189
225
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Menus Only
|
|
228
|
+
* Since we can't have duplicated slugs, if the ID doesn't exist we also check for matching slugs.
|
|
229
|
+
*/
|
|
230
|
+
if( 'menus' === tax && false === idExists ){
|
|
193
231
|
idExists = await axios.request(
|
|
194
232
|
{
|
|
195
233
|
...request,
|
|
@@ -202,37 +240,21 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
|
|
|
202
240
|
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
existingID =
|
|
211
|
-
}else{
|
|
212
|
-
url = `${url}/${ obj.id }`;
|
|
243
|
+
// if the ID exists we append it to the url.
|
|
244
|
+
if( idExists ){
|
|
245
|
+
url = `${url}/${ idExists }`;
|
|
246
|
+
|
|
247
|
+
// we store the existing ID for later use.
|
|
248
|
+
existingID = idExists;
|
|
213
249
|
}
|
|
214
250
|
|
|
215
251
|
// id has to be deleted.
|
|
216
252
|
delete obj.id;
|
|
217
253
|
}
|
|
218
254
|
|
|
219
|
-
// process properties
|
|
220
|
-
if( obj.content && obj.content.rendered ){
|
|
221
|
-
obj.content = processData(obj.content.rendered);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if( obj.title && obj.title.rendered ){
|
|
225
|
-
obj.title = processData(obj.title.rendered);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if( obj.caption && obj.caption.rendered ){
|
|
229
|
-
obj.caption = processData(obj.caption.rendered);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
255
|
// make WordPress REST API request.
|
|
233
256
|
let results = await axios.request({
|
|
234
257
|
...request,
|
|
235
|
-
timeout: 7500,
|
|
236
258
|
data: 'media' === tax ? createMediaItem(obj) : obj,
|
|
237
259
|
url
|
|
238
260
|
})
|
|
@@ -240,25 +262,43 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
|
|
|
240
262
|
|
|
241
263
|
|
|
242
264
|
/**
|
|
243
|
-
* if the obj had an existing ID
|
|
265
|
+
* if the obj had an existing ID we make a request to our plugin endpoint to update the IDs.
|
|
244
266
|
*/
|
|
245
267
|
if( existingID ){
|
|
268
|
+
let extraArgs = {};
|
|
269
|
+
|
|
270
|
+
// if media
|
|
271
|
+
if( 'media' === tax ){
|
|
272
|
+
// get original source domain
|
|
273
|
+
let sourceDomain = obj.source_url.substring(0, obj.source_url.indexOf('/wp-content/uploads'));
|
|
274
|
+
// get expected guid, replace source domain with the new domain.
|
|
275
|
+
let expectedGuid = obj.source_url.replace(sourceDomain, request.url);
|
|
276
|
+
|
|
277
|
+
extraArgs = {
|
|
278
|
+
guid: results.guid.rendered,
|
|
279
|
+
newGuid: expectedGuid,
|
|
280
|
+
// if media and source_url is different from the guid, we update the guid.
|
|
281
|
+
media_details: results.media_details,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
246
284
|
|
|
247
285
|
await axios.request({
|
|
248
286
|
...request,
|
|
249
|
-
url: `${request.url}
|
|
250
|
-
timeout: 7500,
|
|
287
|
+
url: `${request.url}${syncEndpoint}`,
|
|
251
288
|
data: {
|
|
289
|
+
...extraArgs,
|
|
252
290
|
id: results.id,
|
|
253
291
|
newId: existingID,
|
|
254
292
|
tax,
|
|
255
|
-
locations: 'menus' === tax ? obj.locations : []
|
|
293
|
+
locations: 'menus' === tax ? obj.locations : [],
|
|
294
|
+
|
|
256
295
|
}
|
|
257
296
|
})
|
|
258
297
|
.then( async (res) => { return res.data; } )
|
|
259
298
|
|
|
260
299
|
// update the API results ID, back to the existing ID from earlier.
|
|
261
|
-
|
|
300
|
+
results.id = existingID;
|
|
301
|
+
|
|
262
302
|
}
|
|
263
303
|
|
|
264
304
|
// add results to collection
|
|
@@ -275,8 +315,13 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
|
|
|
275
315
|
|
|
276
316
|
}
|
|
277
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Create a FormData from the Media Item Object
|
|
320
|
+
*
|
|
321
|
+
* @param {Object} media Media Object.
|
|
322
|
+
* @returns {FormData}
|
|
323
|
+
*/
|
|
278
324
|
function createMediaItem( media ){
|
|
279
|
-
|
|
280
325
|
let fd = new FormData();
|
|
281
326
|
let featureMediaFile = new File(
|
|
282
327
|
[
|
|
@@ -288,10 +333,12 @@ function createMediaItem( media ){
|
|
|
288
333
|
}
|
|
289
334
|
);
|
|
290
335
|
fd.append('file', featureMediaFile );
|
|
336
|
+
|
|
291
337
|
fd.append('title', media.title);
|
|
292
|
-
fd.append('caption', media.caption);
|
|
338
|
+
fd.append('caption', media.caption );
|
|
293
339
|
fd.append('alt_text', media.alt_text);
|
|
294
340
|
fd.append('date', media.date);
|
|
341
|
+
fd.append('media_details', media.media_details);
|
|
295
342
|
|
|
296
343
|
return fd;
|
|
297
344
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caweb/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "CAWebPublishing Command Line Interface.",
|
|
5
5
|
"exports": "./lib/env.js",
|
|
6
6
|
"type": "module",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"accessibility-checker": "^3.1.67",
|
|
63
63
|
"autoprefixer": "^10.4.17",
|
|
64
64
|
"axios": "^1.6.7",
|
|
65
|
+
"axios-retry": "^4.0.0",
|
|
65
66
|
"chalk": "^5.3.0",
|
|
66
67
|
"commander": "^12.0.0",
|
|
67
68
|
"cross-spawn": "^7.0.3",
|
package/bin/wp-cli.phar
DELETED
|
Binary file
|