@loopress/cli 0.16.0 → 0.17.0
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 +102 -26
- package/dist/commands/seo/list.d.ts +11 -0
- package/dist/commands/seo/list.js +60 -0
- package/dist/commands/seo/pull.d.ts +19 -0
- package/dist/commands/seo/pull.js +134 -0
- package/dist/commands/seo/push.d.ts +20 -0
- package/dist/commands/seo/push.js +171 -0
- package/dist/lib/base.d.ts +1 -0
- package/dist/lib/base.js +5 -0
- package/dist/types/project-config.generated.d.ts +4 -0
- package/dist/utils/seo-format.d.ts +20 -0
- package/dist/utils/seo-format.js +10 -0
- package/oclif.manifest.json +258 -121
- package/package.json +4 -1
- package/schemas/project-config.schema.json +5 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ $ npm install -g @loopress/cli
|
|
|
21
21
|
$ lps COMMAND
|
|
22
22
|
running command...
|
|
23
23
|
$ lps (--version)
|
|
24
|
-
@loopress/cli/0.
|
|
24
|
+
@loopress/cli/0.17.0 linux-x64 node-v24.18.0
|
|
25
25
|
$ lps --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ lps COMMAND
|
|
@@ -66,6 +66,9 @@ report.
|
|
|
66
66
|
* [`lps project push`](#lps-project-push)
|
|
67
67
|
* [`lps project remove`](#lps-project-remove)
|
|
68
68
|
* [`lps project switch`](#lps-project-switch)
|
|
69
|
+
* [`lps seo list`](#lps-seo-list)
|
|
70
|
+
* [`lps seo pull [PATH]`](#lps-seo-pull-path)
|
|
71
|
+
* [`lps seo push [PATH]`](#lps-seo-push-path)
|
|
69
72
|
* [`lps snippet list`](#lps-snippet-list)
|
|
70
73
|
* [`lps snippet publish [PATH]`](#lps-snippet-publish-path)
|
|
71
74
|
* [`lps snippet pull [PATH]`](#lps-snippet-pull-path)
|
|
@@ -96,7 +99,7 @@ EXAMPLES
|
|
|
96
99
|
$ lps acf list --type field-groups
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
_See code: [src/commands/acf/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
102
|
+
_See code: [src/commands/acf/list.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/list.ts)_
|
|
100
103
|
|
|
101
104
|
## `lps acf pull [PATH]`
|
|
102
105
|
|
|
@@ -123,7 +126,7 @@ EXAMPLES
|
|
|
123
126
|
$ lps acf pull --type field-groups
|
|
124
127
|
```
|
|
125
128
|
|
|
126
|
-
_See code: [src/commands/acf/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
129
|
+
_See code: [src/commands/acf/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/pull.ts)_
|
|
127
130
|
|
|
128
131
|
## `lps acf push [PATH]`
|
|
129
132
|
|
|
@@ -150,7 +153,7 @@ EXAMPLES
|
|
|
150
153
|
$ lps acf push --type field-groups
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
_See code: [src/commands/acf/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
156
|
+
_See code: [src/commands/acf/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/push.ts)_
|
|
154
157
|
|
|
155
158
|
## `lps composer init`
|
|
156
159
|
|
|
@@ -172,7 +175,7 @@ EXAMPLES
|
|
|
172
175
|
$ lps composer init --dry-run
|
|
173
176
|
```
|
|
174
177
|
|
|
175
|
-
_See code: [src/commands/composer/init.ts](https://github.com/loopress/loopress/blob/v0.
|
|
178
|
+
_See code: [src/commands/composer/init.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/composer/init.ts)_
|
|
176
179
|
|
|
177
180
|
## `lps composer pull`
|
|
178
181
|
|
|
@@ -194,7 +197,7 @@ EXAMPLES
|
|
|
194
197
|
$ lps composer pull --dry-run
|
|
195
198
|
```
|
|
196
199
|
|
|
197
|
-
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
200
|
+
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/composer/pull.ts)_
|
|
198
201
|
|
|
199
202
|
## `lps composer push`
|
|
200
203
|
|
|
@@ -216,7 +219,7 @@ EXAMPLES
|
|
|
216
219
|
$ lps composer push --dry-run
|
|
217
220
|
```
|
|
218
221
|
|
|
219
|
-
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
222
|
+
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/composer/push.ts)_
|
|
220
223
|
|
|
221
224
|
## `lps help [COMMAND]`
|
|
222
225
|
|
|
@@ -253,7 +256,7 @@ EXAMPLES
|
|
|
253
256
|
$ lps init
|
|
254
257
|
```
|
|
255
258
|
|
|
256
|
-
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.
|
|
259
|
+
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/init.ts)_
|
|
257
260
|
|
|
258
261
|
## `lps login`
|
|
259
262
|
|
|
@@ -270,7 +273,7 @@ EXAMPLES
|
|
|
270
273
|
$ lps login
|
|
271
274
|
```
|
|
272
275
|
|
|
273
|
-
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.
|
|
276
|
+
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/login.ts)_
|
|
274
277
|
|
|
275
278
|
## `lps logout`
|
|
276
279
|
|
|
@@ -287,7 +290,7 @@ EXAMPLES
|
|
|
287
290
|
$ lps logout
|
|
288
291
|
```
|
|
289
292
|
|
|
290
|
-
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.
|
|
293
|
+
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/logout.ts)_
|
|
291
294
|
|
|
292
295
|
## `lps plugin add SLUG`
|
|
293
296
|
|
|
@@ -312,7 +315,7 @@ EXAMPLES
|
|
|
312
315
|
$ lps plugin add contact-form-7 --dry-run
|
|
313
316
|
```
|
|
314
317
|
|
|
315
|
-
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.
|
|
318
|
+
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/plugin/add.ts)_
|
|
316
319
|
|
|
317
320
|
## `lps plugin pull`
|
|
318
321
|
|
|
@@ -334,7 +337,7 @@ EXAMPLES
|
|
|
334
337
|
$ lps plugin pull --dry-run
|
|
335
338
|
```
|
|
336
339
|
|
|
337
|
-
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
340
|
+
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/plugin/pull.ts)_
|
|
338
341
|
|
|
339
342
|
## `lps plugin push`
|
|
340
343
|
|
|
@@ -356,7 +359,7 @@ EXAMPLES
|
|
|
356
359
|
$ lps plugin push --dry-run
|
|
357
360
|
```
|
|
358
361
|
|
|
359
|
-
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
362
|
+
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/plugin/push.ts)_
|
|
360
363
|
|
|
361
364
|
## `lps project config`
|
|
362
365
|
|
|
@@ -374,7 +377,7 @@ EXAMPLES
|
|
|
374
377
|
$ lps project config
|
|
375
378
|
```
|
|
376
379
|
|
|
377
|
-
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.
|
|
380
|
+
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/config.ts)_
|
|
378
381
|
|
|
379
382
|
## `lps project list`
|
|
380
383
|
|
|
@@ -391,7 +394,7 @@ EXAMPLES
|
|
|
391
394
|
$ lps project list
|
|
392
395
|
```
|
|
393
396
|
|
|
394
|
-
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
397
|
+
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/list.ts)_
|
|
395
398
|
|
|
396
399
|
## `lps project pull`
|
|
397
400
|
|
|
@@ -408,7 +411,7 @@ EXAMPLES
|
|
|
408
411
|
$ lps project pull
|
|
409
412
|
```
|
|
410
413
|
|
|
411
|
-
_See code: [src/commands/project/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
414
|
+
_See code: [src/commands/project/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/pull.ts)_
|
|
412
415
|
|
|
413
416
|
## `lps project push`
|
|
414
417
|
|
|
@@ -425,7 +428,7 @@ EXAMPLES
|
|
|
425
428
|
$ lps project push
|
|
426
429
|
```
|
|
427
430
|
|
|
428
|
-
_See code: [src/commands/project/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
431
|
+
_See code: [src/commands/project/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/push.ts)_
|
|
429
432
|
|
|
430
433
|
## `lps project remove`
|
|
431
434
|
|
|
@@ -442,7 +445,7 @@ EXAMPLES
|
|
|
442
445
|
$ lps project remove
|
|
443
446
|
```
|
|
444
447
|
|
|
445
|
-
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.
|
|
448
|
+
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/remove.ts)_
|
|
446
449
|
|
|
447
450
|
## `lps project switch`
|
|
448
451
|
|
|
@@ -459,7 +462,80 @@ EXAMPLES
|
|
|
459
462
|
$ lps project switch
|
|
460
463
|
```
|
|
461
464
|
|
|
462
|
-
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.
|
|
465
|
+
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/project/switch.ts)_
|
|
466
|
+
|
|
467
|
+
## `lps seo list`
|
|
468
|
+
|
|
469
|
+
List posts with SEO meta, and redirects if supported by the active SEO plugin, on WordPress
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
USAGE
|
|
473
|
+
$ lps seo list [-j] [--post-type <value>...]
|
|
474
|
+
|
|
475
|
+
FLAGS
|
|
476
|
+
-j, --json Output in JSON format
|
|
477
|
+
--post-type=<value>... Limit to specific post types
|
|
478
|
+
|
|
479
|
+
DESCRIPTION
|
|
480
|
+
List posts with SEO meta, and redirects if supported by the active SEO plugin, on WordPress
|
|
481
|
+
|
|
482
|
+
EXAMPLES
|
|
483
|
+
$ lps seo list
|
|
484
|
+
|
|
485
|
+
$ lps seo list --post-type post
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
_See code: [src/commands/seo/list.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/seo/list.ts)_
|
|
489
|
+
|
|
490
|
+
## `lps seo pull [PATH]`
|
|
491
|
+
|
|
492
|
+
Pull SEO settings, post meta, and (if supported) redirects from WordPress
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
USAGE
|
|
496
|
+
$ lps seo pull [PATH] [-d] [--post-type <value>...]
|
|
497
|
+
|
|
498
|
+
ARGUMENTS
|
|
499
|
+
[PATH] Path to SEO directory (overrides project config)
|
|
500
|
+
|
|
501
|
+
FLAGS
|
|
502
|
+
-d, --dry-run Show what would change without making changes
|
|
503
|
+
--post-type=<value>... Limit post meta to specific post types
|
|
504
|
+
|
|
505
|
+
DESCRIPTION
|
|
506
|
+
Pull SEO settings, post meta, and (if supported) redirects from WordPress
|
|
507
|
+
|
|
508
|
+
EXAMPLES
|
|
509
|
+
$ lps seo pull
|
|
510
|
+
|
|
511
|
+
$ lps seo pull --post-type post --post-type page
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
_See code: [src/commands/seo/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/seo/pull.ts)_
|
|
515
|
+
|
|
516
|
+
## `lps seo push [PATH]`
|
|
517
|
+
|
|
518
|
+
Push SEO settings, post meta, and redirects to WordPress. Local redirect files created remotely are renamed on disk to the `<id>-<slug>` convention. Fails clearly per file if the active SEO plugin does not support redirects.
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
USAGE
|
|
522
|
+
$ lps seo push [PATH] [-d]
|
|
523
|
+
|
|
524
|
+
ARGUMENTS
|
|
525
|
+
[PATH] Path to SEO directory (overrides project config)
|
|
526
|
+
|
|
527
|
+
FLAGS
|
|
528
|
+
-d, --dry-run Show what would change without making changes
|
|
529
|
+
|
|
530
|
+
DESCRIPTION
|
|
531
|
+
Push SEO settings, post meta, and redirects to WordPress. Local redirect files created remotely are renamed on disk to
|
|
532
|
+
the `<id>-<slug>` convention. Fails clearly per file if the active SEO plugin does not support redirects.
|
|
533
|
+
|
|
534
|
+
EXAMPLES
|
|
535
|
+
$ lps seo push
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
_See code: [src/commands/seo/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/seo/push.ts)_
|
|
463
539
|
|
|
464
540
|
## `lps snippet list`
|
|
465
541
|
|
|
@@ -479,7 +555,7 @@ EXAMPLES
|
|
|
479
555
|
$ lps snippet list
|
|
480
556
|
```
|
|
481
557
|
|
|
482
|
-
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
558
|
+
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/snippet/list.ts)_
|
|
483
559
|
|
|
484
560
|
## `lps snippet publish [PATH]`
|
|
485
561
|
|
|
@@ -502,7 +578,7 @@ EXAMPLES
|
|
|
502
578
|
$ lps snippet publish --path ./snippets
|
|
503
579
|
```
|
|
504
580
|
|
|
505
|
-
_See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.
|
|
581
|
+
_See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/snippet/publish.ts)_
|
|
506
582
|
|
|
507
583
|
## `lps snippet pull [PATH]`
|
|
508
584
|
|
|
@@ -527,7 +603,7 @@ EXAMPLES
|
|
|
527
603
|
$ lps snippet pull --path ./snippets
|
|
528
604
|
```
|
|
529
605
|
|
|
530
|
-
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
606
|
+
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/snippet/pull.ts)_
|
|
531
607
|
|
|
532
608
|
## `lps snippet push [PATH]`
|
|
533
609
|
|
|
@@ -553,7 +629,7 @@ EXAMPLES
|
|
|
553
629
|
$ lps snippet push --path ./snippets
|
|
554
630
|
```
|
|
555
631
|
|
|
556
|
-
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
632
|
+
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/snippet/push.ts)_
|
|
557
633
|
|
|
558
634
|
## `lps status`
|
|
559
635
|
|
|
@@ -570,7 +646,7 @@ EXAMPLES
|
|
|
570
646
|
$ lps status
|
|
571
647
|
```
|
|
572
648
|
|
|
573
|
-
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.
|
|
649
|
+
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/status.ts)_
|
|
574
650
|
|
|
575
651
|
## `lps telemetry disable`
|
|
576
652
|
|
|
@@ -587,7 +663,7 @@ EXAMPLES
|
|
|
587
663
|
$ lps telemetry disable
|
|
588
664
|
```
|
|
589
665
|
|
|
590
|
-
_See code: [src/commands/telemetry/disable.ts](https://github.com/loopress/loopress/blob/v0.
|
|
666
|
+
_See code: [src/commands/telemetry/disable.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/telemetry/disable.ts)_
|
|
591
667
|
|
|
592
668
|
## `lps telemetry enable`
|
|
593
669
|
|
|
@@ -604,5 +680,5 @@ EXAMPLES
|
|
|
604
680
|
$ lps telemetry enable
|
|
605
681
|
```
|
|
606
682
|
|
|
607
|
-
_See code: [src/commands/telemetry/enable.ts](https://github.com/loopress/loopress/blob/v0.
|
|
683
|
+
_See code: [src/commands/telemetry/enable.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/telemetry/enable.ts)_
|
|
608
684
|
<!-- commandsstop -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
export default class List extends LoopressCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
'post-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
private fetchRedirects;
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { DEFAULT_POST_TYPES, SEO_REDIRECTS_ENDPOINT, seoPostMetaEndpoint } from '../../utils/seo-format.js';
|
|
4
|
+
export default class List extends LoopressCommand {
|
|
5
|
+
static description = 'List posts with SEO meta, and redirects if supported by the active SEO plugin, on WordPress';
|
|
6
|
+
static examples = ['$ lps seo list', '$ lps seo list --post-type post'];
|
|
7
|
+
static flags = {
|
|
8
|
+
json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
|
|
9
|
+
'post-type': Flags.string({ description: 'Limit to specific post types', multiple: true }),
|
|
10
|
+
};
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(List);
|
|
13
|
+
const postTypes = flags['post-type'] && flags['post-type'].length > 0 ? flags['post-type'] : [...DEFAULT_POST_TYPES];
|
|
14
|
+
const byType = {};
|
|
15
|
+
for (const postType of postTypes) {
|
|
16
|
+
byType[postType] = await this.wp.get(seoPostMetaEndpoint(postType));
|
|
17
|
+
}
|
|
18
|
+
const { redirects, unsupportedReason } = await this.fetchRedirects();
|
|
19
|
+
if (flags.json) {
|
|
20
|
+
this.log(JSON.stringify({ postMeta: byType, redirects: redirects ?? undefined, redirectsUnsupported: unsupportedReason }, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const postType of postTypes) {
|
|
24
|
+
const posts = byType[postType];
|
|
25
|
+
this.log(`${postType} (${posts.length}):`);
|
|
26
|
+
if (posts.length === 0) {
|
|
27
|
+
this.log(' (none)');
|
|
28
|
+
this.log('');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const post of posts) {
|
|
32
|
+
this.log(` ${post.slug}. ${post.title}`);
|
|
33
|
+
}
|
|
34
|
+
this.log('');
|
|
35
|
+
}
|
|
36
|
+
if (unsupportedReason) {
|
|
37
|
+
this.log(`redirects: ${unsupportedReason}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.log(`redirects (${redirects.length}):`);
|
|
41
|
+
if (redirects.length === 0) {
|
|
42
|
+
this.log(' (none)');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const redirect of redirects) {
|
|
46
|
+
this.log(` ${redirect.id}. [${redirect.status}] ${redirect.headerCode} -> ${redirect.urlTo}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Redirects are only supported by some SeoProvider backends (RankMath, not Yoast); reported
|
|
50
|
+
// as a line in the listing rather than failing the whole command, since post meta above may
|
|
51
|
+
// well have succeeded.
|
|
52
|
+
async fetchRedirects() {
|
|
53
|
+
try {
|
|
54
|
+
return { redirects: await this.wp.get(SEO_REDIRECTS_ENDPOINT), unsupportedReason: undefined };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return { redirects: null, unsupportedReason: error.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { SeoRedirect } from '../../utils/seo-format.js';
|
|
3
|
+
export default class Pull extends LoopressCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
'post-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private findOrphanedFiles;
|
|
15
|
+
private pullPostMeta;
|
|
16
|
+
private pullRedirects;
|
|
17
|
+
private pullSettings;
|
|
18
|
+
}
|
|
19
|
+
export declare function redirectFileBase(redirect: SeoRedirect): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { Listr } from 'listr2';
|
|
3
|
+
import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { extname, join } from 'node:path';
|
|
5
|
+
import slugify from 'slugify';
|
|
6
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
7
|
+
import { DEFAULT_POST_TYPES, SEO_REDIRECTS_ENDPOINT, SEO_SETTINGS_ENDPOINT, seoPostMetaEndpoint, } from '../../utils/seo-format.js';
|
|
8
|
+
export default class Pull extends LoopressCommand {
|
|
9
|
+
static args = {
|
|
10
|
+
path: Args.string({ description: 'Path to SEO directory (overrides project config)' }),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Pull SEO settings, post meta, and (if supported) redirects from WordPress';
|
|
13
|
+
static examples = ['$ lps seo pull', '$ lps seo pull --post-type post --post-type page'];
|
|
14
|
+
static flags = {
|
|
15
|
+
...LoopressCommand.dryRunFlag,
|
|
16
|
+
'post-type': Flags.string({ description: 'Limit post meta to specific post types', multiple: true }),
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(Pull);
|
|
20
|
+
const { url } = this.siteConfig;
|
|
21
|
+
const path = this.resolveSeoPath(args.path);
|
|
22
|
+
const postTypes = flags['post-type'] && flags['post-type'].length > 0 ? flags['post-type'] : [...DEFAULT_POST_TYPES];
|
|
23
|
+
this.log(`Pulling SEO configuration from ${url}`);
|
|
24
|
+
this.log(`SEO path: ${path}`);
|
|
25
|
+
await this.pullSettings(path);
|
|
26
|
+
for (const postType of postTypes) {
|
|
27
|
+
await this.pullPostMeta(postType, path);
|
|
28
|
+
}
|
|
29
|
+
await this.pullRedirects(path);
|
|
30
|
+
}
|
|
31
|
+
// Shared by post-meta (`<slug>.json`) and redirects (`<id>-<slug>.json`) directories: a local
|
|
32
|
+
// file whose identity is no longer in the current remote list belongs to something deleted on
|
|
33
|
+
// WordPress. Left on disk, it would silently come back to life on the next `seo push`.
|
|
34
|
+
async findOrphanedFiles(dir, keepKeys, numericIdPrefix) {
|
|
35
|
+
let files;
|
|
36
|
+
try {
|
|
37
|
+
files = await readdir(dir);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT')
|
|
41
|
+
return [];
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return files.filter((file) => {
|
|
45
|
+
if (extname(file) !== '.json')
|
|
46
|
+
return false;
|
|
47
|
+
if (numericIdPrefix) {
|
|
48
|
+
const match = /^(\d+)-/.exec(file);
|
|
49
|
+
return match !== null && !keepKeys.has(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return !keepKeys.has(file.slice(0, -'.json'.length));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async pullPostMeta(postType, basePath) {
|
|
55
|
+
const dir = join(basePath, 'post-meta', postType);
|
|
56
|
+
const remote = await this.wp.get(seoPostMetaEndpoint(postType));
|
|
57
|
+
const orphans = await this.findOrphanedFiles(dir, new Set(remote.map((post) => post.slug)), false);
|
|
58
|
+
if (this.dryRun) {
|
|
59
|
+
this.log(`[dry-run] Would pull ${remote.length} ${postType} post-meta file(s) to ${dir}`);
|
|
60
|
+
if (orphans.length > 0) {
|
|
61
|
+
this.log(`[dry-run] Would remove ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (remote.length > 0)
|
|
66
|
+
await mkdir(dir, { recursive: true });
|
|
67
|
+
await new Listr(remote.map((post) => ({
|
|
68
|
+
async task(_ctx, task) {
|
|
69
|
+
await writeFile(join(dir, `${post.slug}.json`), JSON.stringify(post, null, 2) + '\n');
|
|
70
|
+
task.output = `Pulled: ${post.slug}`;
|
|
71
|
+
},
|
|
72
|
+
title: `Pull ${post.slug}`,
|
|
73
|
+
}))).run();
|
|
74
|
+
for (const file of orphans)
|
|
75
|
+
await rm(join(dir, file), { force: true });
|
|
76
|
+
if (orphans.length > 0) {
|
|
77
|
+
this.warn(`Removed ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
this.log(`Pulled ${remote.length} ${postType} post-meta file(s) to ${dir}`);
|
|
80
|
+
}
|
|
81
|
+
// Redirects are only supported by some SeoProvider backends (RankMath, not Yoast). Unlike
|
|
82
|
+
// push (which must fail loudly if the user has local redirect files that can't be synced),
|
|
83
|
+
// pull degrades gracefully here: the active plugin never supporting redirects isn't an error
|
|
84
|
+
// in the same sense a deleted-on-WordPress file is, there's simply nothing to pull.
|
|
85
|
+
async pullRedirects(basePath) {
|
|
86
|
+
const dir = join(basePath, 'redirects');
|
|
87
|
+
let remote;
|
|
88
|
+
try {
|
|
89
|
+
remote = await this.wp.get(SEO_REDIRECTS_ENDPOINT);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.warn(`Skipping redirects: ${error.message}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const orphans = await this.findOrphanedFiles(dir, new Set(remote.map((redirect) => String(redirect.id))), true);
|
|
96
|
+
if (this.dryRun) {
|
|
97
|
+
this.log(`[dry-run] Would pull ${remote.length} redirect(s) to ${dir}`);
|
|
98
|
+
if (orphans.length > 0) {
|
|
99
|
+
this.log(`[dry-run] Would remove ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (remote.length > 0)
|
|
104
|
+
await mkdir(dir, { recursive: true });
|
|
105
|
+
await new Listr(remote.map((redirect) => ({
|
|
106
|
+
async task(_ctx, task) {
|
|
107
|
+
await writeFile(join(dir, `${redirectFileBase(redirect)}.json`), JSON.stringify(redirect, null, 2) + '\n');
|
|
108
|
+
task.output = `Pulled: redirect #${redirect.id}`;
|
|
109
|
+
},
|
|
110
|
+
title: `Pull redirect #${redirect.id}`,
|
|
111
|
+
}))).run();
|
|
112
|
+
for (const file of orphans)
|
|
113
|
+
await rm(join(dir, file), { force: true });
|
|
114
|
+
if (orphans.length > 0) {
|
|
115
|
+
this.warn(`Removed ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
this.log(`Pulled ${remote.length} redirect(s) to ${dir}`);
|
|
118
|
+
}
|
|
119
|
+
async pullSettings(basePath) {
|
|
120
|
+
const file = join(basePath, 'settings.json');
|
|
121
|
+
const settings = await this.wp.get(SEO_SETTINGS_ENDPOINT);
|
|
122
|
+
if (this.dryRun) {
|
|
123
|
+
this.log(`[dry-run] Would pull settings to ${file}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await mkdir(basePath, { recursive: true });
|
|
127
|
+
await writeFile(file, JSON.stringify(settings, null, 2) + '\n');
|
|
128
|
+
this.log(`Pulled settings to ${file}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function redirectFileBase(redirect) {
|
|
132
|
+
const slug = slugify(redirect.urlTo || 'redirect', { lower: true, strict: true });
|
|
133
|
+
return `${redirect.id}-${slug || 'redirect'}`;
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
export default class Push extends PushCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
private failedCount;
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private jsonFilesIn;
|
|
14
|
+
private pushPostMeta;
|
|
15
|
+
private pushPostMetaFile;
|
|
16
|
+
private pushRedirectFile;
|
|
17
|
+
private pushRedirects;
|
|
18
|
+
private pushSettings;
|
|
19
|
+
private renameToCanonical;
|
|
20
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { Listr } from 'listr2';
|
|
3
|
+
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, extname, join } from 'node:path';
|
|
5
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
6
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
+
import { SEO_REDIRECTS_ENDPOINT, SEO_SETTINGS_ENDPOINT, seoPostMetaEndpoint, seoRedirectEndpoint, } from '../../utils/seo-format.js';
|
|
8
|
+
import { redirectFileBase } from './pull.js';
|
|
9
|
+
export default class Push extends PushCommand {
|
|
10
|
+
static args = {
|
|
11
|
+
path: Args.string({ description: 'Path to SEO directory (overrides project config)' }),
|
|
12
|
+
};
|
|
13
|
+
static description = 'Push SEO settings, post meta, and redirects to WordPress. Local redirect files created remotely are renamed on disk to the `<id>-<slug>` convention. Fails clearly per file if the active SEO plugin does not support redirects.';
|
|
14
|
+
static examples = ['$ lps seo push'];
|
|
15
|
+
static flags = {
|
|
16
|
+
...PushCommand.dryRunFlag,
|
|
17
|
+
};
|
|
18
|
+
failedCount = 0;
|
|
19
|
+
async run() {
|
|
20
|
+
const { args } = await this.parse(Push);
|
|
21
|
+
const { url } = this.siteConfig;
|
|
22
|
+
const path = this.resolveSeoPath(args.path);
|
|
23
|
+
this.log(`Pushing SEO configuration to ${url}`);
|
|
24
|
+
this.log(`SEO path: ${path}`);
|
|
25
|
+
await this.pushSettings(path);
|
|
26
|
+
await this.pushPostMeta(path);
|
|
27
|
+
await this.pushRedirects(path);
|
|
28
|
+
if (this.failedCount > 0) {
|
|
29
|
+
this.error(`${this.failedCount} SEO item${this.failedCount === 1 ? '' : 's'} failed to push.`);
|
|
30
|
+
}
|
|
31
|
+
if (this.dryRun)
|
|
32
|
+
return;
|
|
33
|
+
await this.recordSuccess();
|
|
34
|
+
this.log('All SEO configuration pushed.');
|
|
35
|
+
}
|
|
36
|
+
async jsonFilesIn(dir) {
|
|
37
|
+
try {
|
|
38
|
+
return (await readdir(dir)).filter((file) => extname(file) === '.json');
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code === 'ENOENT')
|
|
42
|
+
return [];
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async pushPostMeta(basePath) {
|
|
47
|
+
const root = join(basePath, 'post-meta');
|
|
48
|
+
let postTypeDirs;
|
|
49
|
+
try {
|
|
50
|
+
postTypeDirs = (await readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === 'ENOENT')
|
|
54
|
+
return;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
for (const postType of postTypeDirs) {
|
|
58
|
+
const dir = join(root, postType);
|
|
59
|
+
const files = await this.jsonFilesIn(dir);
|
|
60
|
+
if (files.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
this.log(`Found ${files.length} ${postType} post-meta file${files.length === 1 ? '' : 's'} to push`);
|
|
63
|
+
await new Listr(files.map((file) => ({
|
|
64
|
+
task: async (_ctx, task) => this.pushPostMetaFile(postType, join(dir, file), task),
|
|
65
|
+
title: `Push ${file}`,
|
|
66
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async pushPostMetaFile(postType, filePath, task) {
|
|
70
|
+
if (this.dryRun) {
|
|
71
|
+
if (task)
|
|
72
|
+
task.output = `[dry-run] Would push: ${filePath}`;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const post = JSON.parse(await readFile(filePath, 'utf8'));
|
|
77
|
+
await this.wp.post(seoPostMetaEndpoint(postType), { meta: post.meta, slug: post.slug });
|
|
78
|
+
if (task)
|
|
79
|
+
task.output = `Pushed: ${post.slug}`;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const message = `Failed to push ${filePath}: ${error.message}`;
|
|
83
|
+
if (task)
|
|
84
|
+
task.output = message;
|
|
85
|
+
else
|
|
86
|
+
this.warn(` ${message}`);
|
|
87
|
+
this.failedCount++;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async pushRedirectFile(filePath, task) {
|
|
92
|
+
if (this.dryRun) {
|
|
93
|
+
if (task)
|
|
94
|
+
task.output = `[dry-run] Would push: ${filePath}`;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const redirect = JSON.parse(await readFile(filePath, 'utf8'));
|
|
99
|
+
const payload = { headerCode: redirect.headerCode, sources: redirect.sources, status: redirect.status, urlTo: redirect.urlTo };
|
|
100
|
+
if (redirect.id) {
|
|
101
|
+
try {
|
|
102
|
+
await this.wp.put(seoRedirectEndpoint(redirect.id), payload);
|
|
103
|
+
if (task)
|
|
104
|
+
task.output = `Pushed: redirect #${redirect.id}`;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// The id recorded locally doesn't exist on this site (e.g. a fresh install): create it
|
|
109
|
+
// instead of failing, and adopt whatever id the site assigns (same fallback as snippet push).
|
|
110
|
+
if (!isNotFoundError(error))
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const created = await this.wp.post(SEO_REDIRECTS_ENDPOINT, payload);
|
|
115
|
+
await this.renameToCanonical(filePath, created);
|
|
116
|
+
if (task)
|
|
117
|
+
task.output = `Pushed: redirect #${created.id}`;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const message = `Failed to push ${filePath}: ${error.message}`;
|
|
121
|
+
if (task)
|
|
122
|
+
task.output = message;
|
|
123
|
+
else
|
|
124
|
+
this.warn(` ${message}`);
|
|
125
|
+
this.failedCount++;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async pushRedirects(basePath) {
|
|
130
|
+
const dir = join(basePath, 'redirects');
|
|
131
|
+
const files = await this.jsonFilesIn(dir);
|
|
132
|
+
if (files.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
this.log(`Found ${files.length} redirect${files.length === 1 ? '' : 's'} to push`);
|
|
135
|
+
await new Listr(files.map((file) => ({
|
|
136
|
+
task: async (_ctx, task) => this.pushRedirectFile(join(dir, file), task),
|
|
137
|
+
title: `Push ${file}`,
|
|
138
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
139
|
+
}
|
|
140
|
+
async pushSettings(basePath) {
|
|
141
|
+
const file = join(basePath, 'settings.json');
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = await readFile(file, 'utf8');
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code === 'ENOENT')
|
|
148
|
+
return;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
if (this.dryRun) {
|
|
152
|
+
this.log(`[dry-run] Would push: ${file}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await this.wp.put(SEO_SETTINGS_ENDPOINT, JSON.parse(raw));
|
|
157
|
+
this.log(`Pushed: ${file}`);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.failedCount++;
|
|
161
|
+
this.warn(`Failed to push ${file}: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async renameToCanonical(filePath, redirect) {
|
|
165
|
+
const dir = dirname(filePath);
|
|
166
|
+
const canonicalPath = join(dir, `${redirectFileBase(redirect)}.json`);
|
|
167
|
+
await writeFile(canonicalPath, JSON.stringify(redirect, null, 2) + '\n');
|
|
168
|
+
if (canonicalPath !== filePath)
|
|
169
|
+
await rm(filePath, { force: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
14
14
|
protected get wp(): WpClient;
|
|
15
15
|
init(): Promise<void>;
|
|
16
16
|
protected resolveAcfPath(override?: string): string;
|
|
17
|
+
protected resolveSeoPath(override?: string): string;
|
|
17
18
|
protected resolveSnippetsPath(override?: string): string;
|
|
18
19
|
private resolveEnvironment;
|
|
19
20
|
}
|
package/dist/lib/base.js
CHANGED
|
@@ -40,6 +40,11 @@ export class LoopressCommand extends Command {
|
|
|
40
40
|
return override;
|
|
41
41
|
return join(this.rootDir, this.localConfig.acfDir ?? 'acf');
|
|
42
42
|
}
|
|
43
|
+
resolveSeoPath(override) {
|
|
44
|
+
if (override)
|
|
45
|
+
return override;
|
|
46
|
+
return join(this.rootDir, this.localConfig.seoDir ?? 'seo');
|
|
47
|
+
}
|
|
43
48
|
resolveSnippetsPath(override) {
|
|
44
49
|
if (override)
|
|
45
50
|
return override;
|
|
@@ -15,6 +15,10 @@ export interface LoopressProjectConfiguration {
|
|
|
15
15
|
* Directory where ACF field groups, post types, taxonomies and options pages are read from and written to, relative to rootDir.
|
|
16
16
|
*/
|
|
17
17
|
acfDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Directory where SEO settings, post meta, and redirects (RankMath and Yoast SEO, whichever is active) are read from and written to, relative to rootDir.
|
|
20
|
+
*/
|
|
21
|
+
seoDir?: string;
|
|
18
22
|
/**
|
|
19
23
|
* Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config.
|
|
20
24
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const DEFAULT_POST_TYPES: readonly ["post", "page"];
|
|
2
|
+
export declare const SEO_SETTINGS_ENDPOINT = "loopress/v1/seo/settings";
|
|
3
|
+
export declare const SEO_REDIRECTS_ENDPOINT = "loopress/v1/seo/redirects";
|
|
4
|
+
export declare function seoRedirectEndpoint(id: number): string;
|
|
5
|
+
export declare function seoPostMetaEndpoint(postType: string): string;
|
|
6
|
+
export interface SeoPostMeta {
|
|
7
|
+
meta: Record<string, unknown>;
|
|
8
|
+
slug: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SeoRedirect {
|
|
12
|
+
createdAt: null | string;
|
|
13
|
+
headerCode: number;
|
|
14
|
+
hits: number;
|
|
15
|
+
id: number;
|
|
16
|
+
sources: unknown;
|
|
17
|
+
status: string;
|
|
18
|
+
updatedAt: null | string;
|
|
19
|
+
urlTo: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Post types synced by `lps seo pull` when --post-type isn't given.
|
|
2
|
+
export const DEFAULT_POST_TYPES = ['post', 'page'];
|
|
3
|
+
export const SEO_SETTINGS_ENDPOINT = 'loopress/v1/seo/settings';
|
|
4
|
+
export const SEO_REDIRECTS_ENDPOINT = 'loopress/v1/seo/redirects';
|
|
5
|
+
export function seoRedirectEndpoint(id) {
|
|
6
|
+
return `${SEO_REDIRECTS_ENDPOINT}/${id}`;
|
|
7
|
+
}
|
|
8
|
+
export function seoPostMetaEndpoint(postType) {
|
|
9
|
+
return `loopress/v1/seo/post-meta/${postType}`;
|
|
10
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -390,6 +390,263 @@
|
|
|
390
390
|
"push.js"
|
|
391
391
|
]
|
|
392
392
|
},
|
|
393
|
+
"plugin:add": {
|
|
394
|
+
"aliases": [],
|
|
395
|
+
"args": {
|
|
396
|
+
"slug": {
|
|
397
|
+
"description": "Plugin slug on WordPress.org",
|
|
398
|
+
"name": "slug",
|
|
399
|
+
"required": true
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"description": "Add a WordPress.org plugin to loopress.json",
|
|
403
|
+
"examples": [
|
|
404
|
+
"$ lps plugin add woocommerce",
|
|
405
|
+
"$ lps plugin add contact-form-7 --dry-run"
|
|
406
|
+
],
|
|
407
|
+
"flags": {
|
|
408
|
+
"dry-run": {
|
|
409
|
+
"char": "d",
|
|
410
|
+
"description": "Show what would change without making changes",
|
|
411
|
+
"name": "dry-run",
|
|
412
|
+
"allowNo": false,
|
|
413
|
+
"type": "boolean"
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"hasDynamicHelp": false,
|
|
417
|
+
"hiddenAliases": [],
|
|
418
|
+
"id": "plugin:add",
|
|
419
|
+
"pluginAlias": "@loopress/cli",
|
|
420
|
+
"pluginName": "@loopress/cli",
|
|
421
|
+
"pluginType": "core",
|
|
422
|
+
"strict": true,
|
|
423
|
+
"enableJsonFlag": false,
|
|
424
|
+
"dryRunFlag": {
|
|
425
|
+
"dry-run": {
|
|
426
|
+
"char": "d",
|
|
427
|
+
"description": "Show what would change without making changes",
|
|
428
|
+
"allowNo": false,
|
|
429
|
+
"type": "boolean"
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
"isESM": true,
|
|
433
|
+
"relativePath": [
|
|
434
|
+
"dist",
|
|
435
|
+
"commands",
|
|
436
|
+
"plugin",
|
|
437
|
+
"add.js"
|
|
438
|
+
]
|
|
439
|
+
},
|
|
440
|
+
"plugin:pull": {
|
|
441
|
+
"aliases": [],
|
|
442
|
+
"args": {},
|
|
443
|
+
"description": "Pull installed plugins from WordPress into loopress.json",
|
|
444
|
+
"examples": [
|
|
445
|
+
"$ lps plugin pull",
|
|
446
|
+
"$ lps plugin pull --dry-run"
|
|
447
|
+
],
|
|
448
|
+
"flags": {
|
|
449
|
+
"dry-run": {
|
|
450
|
+
"char": "d",
|
|
451
|
+
"description": "Show what would change without making changes",
|
|
452
|
+
"name": "dry-run",
|
|
453
|
+
"allowNo": false,
|
|
454
|
+
"type": "boolean"
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
"hasDynamicHelp": false,
|
|
458
|
+
"hiddenAliases": [],
|
|
459
|
+
"id": "plugin:pull",
|
|
460
|
+
"pluginAlias": "@loopress/cli",
|
|
461
|
+
"pluginName": "@loopress/cli",
|
|
462
|
+
"pluginType": "core",
|
|
463
|
+
"strict": true,
|
|
464
|
+
"enableJsonFlag": false,
|
|
465
|
+
"dryRunFlag": {
|
|
466
|
+
"dry-run": {
|
|
467
|
+
"char": "d",
|
|
468
|
+
"description": "Show what would change without making changes",
|
|
469
|
+
"allowNo": false,
|
|
470
|
+
"type": "boolean"
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
"isESM": true,
|
|
474
|
+
"relativePath": [
|
|
475
|
+
"dist",
|
|
476
|
+
"commands",
|
|
477
|
+
"plugin",
|
|
478
|
+
"pull.js"
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
"plugin:push": {
|
|
482
|
+
"aliases": [],
|
|
483
|
+
"args": {},
|
|
484
|
+
"description": "Push plugins to WordPress to match loopress.json",
|
|
485
|
+
"examples": [
|
|
486
|
+
"$ lps plugin push",
|
|
487
|
+
"$ lps plugin push --dry-run"
|
|
488
|
+
],
|
|
489
|
+
"flags": {
|
|
490
|
+
"dry-run": {
|
|
491
|
+
"char": "d",
|
|
492
|
+
"description": "Show what would change without making changes",
|
|
493
|
+
"name": "dry-run",
|
|
494
|
+
"allowNo": false,
|
|
495
|
+
"type": "boolean"
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
"hasDynamicHelp": false,
|
|
499
|
+
"hiddenAliases": [],
|
|
500
|
+
"id": "plugin:push",
|
|
501
|
+
"pluginAlias": "@loopress/cli",
|
|
502
|
+
"pluginName": "@loopress/cli",
|
|
503
|
+
"pluginType": "core",
|
|
504
|
+
"strict": true,
|
|
505
|
+
"isESM": true,
|
|
506
|
+
"relativePath": [
|
|
507
|
+
"dist",
|
|
508
|
+
"commands",
|
|
509
|
+
"plugin",
|
|
510
|
+
"push.js"
|
|
511
|
+
]
|
|
512
|
+
},
|
|
513
|
+
"seo:list": {
|
|
514
|
+
"aliases": [],
|
|
515
|
+
"args": {},
|
|
516
|
+
"description": "List posts with SEO meta, and redirects if supported by the active SEO plugin, on WordPress",
|
|
517
|
+
"examples": [
|
|
518
|
+
"$ lps seo list",
|
|
519
|
+
"$ lps seo list --post-type post"
|
|
520
|
+
],
|
|
521
|
+
"flags": {
|
|
522
|
+
"json": {
|
|
523
|
+
"char": "j",
|
|
524
|
+
"description": "Output in JSON format",
|
|
525
|
+
"name": "json",
|
|
526
|
+
"allowNo": false,
|
|
527
|
+
"type": "boolean"
|
|
528
|
+
},
|
|
529
|
+
"post-type": {
|
|
530
|
+
"description": "Limit to specific post types",
|
|
531
|
+
"name": "post-type",
|
|
532
|
+
"hasDynamicHelp": false,
|
|
533
|
+
"multiple": true,
|
|
534
|
+
"type": "option"
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
"hasDynamicHelp": false,
|
|
538
|
+
"hiddenAliases": [],
|
|
539
|
+
"id": "seo:list",
|
|
540
|
+
"pluginAlias": "@loopress/cli",
|
|
541
|
+
"pluginName": "@loopress/cli",
|
|
542
|
+
"pluginType": "core",
|
|
543
|
+
"strict": true,
|
|
544
|
+
"enableJsonFlag": false,
|
|
545
|
+
"dryRunFlag": {
|
|
546
|
+
"dry-run": {
|
|
547
|
+
"char": "d",
|
|
548
|
+
"description": "Show what would change without making changes",
|
|
549
|
+
"allowNo": false,
|
|
550
|
+
"type": "boolean"
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
"isESM": true,
|
|
554
|
+
"relativePath": [
|
|
555
|
+
"dist",
|
|
556
|
+
"commands",
|
|
557
|
+
"seo",
|
|
558
|
+
"list.js"
|
|
559
|
+
]
|
|
560
|
+
},
|
|
561
|
+
"seo:pull": {
|
|
562
|
+
"aliases": [],
|
|
563
|
+
"args": {
|
|
564
|
+
"path": {
|
|
565
|
+
"description": "Path to SEO directory (overrides project config)",
|
|
566
|
+
"name": "path"
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
"description": "Pull SEO settings, post meta, and (if supported) redirects from WordPress",
|
|
570
|
+
"examples": [
|
|
571
|
+
"$ lps seo pull",
|
|
572
|
+
"$ lps seo pull --post-type post --post-type page"
|
|
573
|
+
],
|
|
574
|
+
"flags": {
|
|
575
|
+
"dry-run": {
|
|
576
|
+
"char": "d",
|
|
577
|
+
"description": "Show what would change without making changes",
|
|
578
|
+
"name": "dry-run",
|
|
579
|
+
"allowNo": false,
|
|
580
|
+
"type": "boolean"
|
|
581
|
+
},
|
|
582
|
+
"post-type": {
|
|
583
|
+
"description": "Limit post meta to specific post types",
|
|
584
|
+
"name": "post-type",
|
|
585
|
+
"hasDynamicHelp": false,
|
|
586
|
+
"multiple": true,
|
|
587
|
+
"type": "option"
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
"hasDynamicHelp": false,
|
|
591
|
+
"hiddenAliases": [],
|
|
592
|
+
"id": "seo:pull",
|
|
593
|
+
"pluginAlias": "@loopress/cli",
|
|
594
|
+
"pluginName": "@loopress/cli",
|
|
595
|
+
"pluginType": "core",
|
|
596
|
+
"strict": true,
|
|
597
|
+
"enableJsonFlag": false,
|
|
598
|
+
"dryRunFlag": {
|
|
599
|
+
"dry-run": {
|
|
600
|
+
"char": "d",
|
|
601
|
+
"description": "Show what would change without making changes",
|
|
602
|
+
"allowNo": false,
|
|
603
|
+
"type": "boolean"
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
"isESM": true,
|
|
607
|
+
"relativePath": [
|
|
608
|
+
"dist",
|
|
609
|
+
"commands",
|
|
610
|
+
"seo",
|
|
611
|
+
"pull.js"
|
|
612
|
+
]
|
|
613
|
+
},
|
|
614
|
+
"seo:push": {
|
|
615
|
+
"aliases": [],
|
|
616
|
+
"args": {
|
|
617
|
+
"path": {
|
|
618
|
+
"description": "Path to SEO directory (overrides project config)",
|
|
619
|
+
"name": "path"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
"description": "Push SEO settings, post meta, and redirects to WordPress. Local redirect files created remotely are renamed on disk to the `<id>-<slug>` convention. Fails clearly per file if the active SEO plugin does not support redirects.",
|
|
623
|
+
"examples": [
|
|
624
|
+
"$ lps seo push"
|
|
625
|
+
],
|
|
626
|
+
"flags": {
|
|
627
|
+
"dry-run": {
|
|
628
|
+
"char": "d",
|
|
629
|
+
"description": "Show what would change without making changes",
|
|
630
|
+
"name": "dry-run",
|
|
631
|
+
"allowNo": false,
|
|
632
|
+
"type": "boolean"
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
"hasDynamicHelp": false,
|
|
636
|
+
"hiddenAliases": [],
|
|
637
|
+
"id": "seo:push",
|
|
638
|
+
"pluginAlias": "@loopress/cli",
|
|
639
|
+
"pluginName": "@loopress/cli",
|
|
640
|
+
"pluginType": "core",
|
|
641
|
+
"strict": true,
|
|
642
|
+
"isESM": true,
|
|
643
|
+
"relativePath": [
|
|
644
|
+
"dist",
|
|
645
|
+
"commands",
|
|
646
|
+
"seo",
|
|
647
|
+
"push.js"
|
|
648
|
+
]
|
|
649
|
+
},
|
|
393
650
|
"project:config": {
|
|
394
651
|
"aliases": [],
|
|
395
652
|
"args": {},
|
|
@@ -687,126 +944,6 @@
|
|
|
687
944
|
"push.js"
|
|
688
945
|
]
|
|
689
946
|
},
|
|
690
|
-
"plugin:add": {
|
|
691
|
-
"aliases": [],
|
|
692
|
-
"args": {
|
|
693
|
-
"slug": {
|
|
694
|
-
"description": "Plugin slug on WordPress.org",
|
|
695
|
-
"name": "slug",
|
|
696
|
-
"required": true
|
|
697
|
-
}
|
|
698
|
-
},
|
|
699
|
-
"description": "Add a WordPress.org plugin to loopress.json",
|
|
700
|
-
"examples": [
|
|
701
|
-
"$ lps plugin add woocommerce",
|
|
702
|
-
"$ lps plugin add contact-form-7 --dry-run"
|
|
703
|
-
],
|
|
704
|
-
"flags": {
|
|
705
|
-
"dry-run": {
|
|
706
|
-
"char": "d",
|
|
707
|
-
"description": "Show what would change without making changes",
|
|
708
|
-
"name": "dry-run",
|
|
709
|
-
"allowNo": false,
|
|
710
|
-
"type": "boolean"
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
"hasDynamicHelp": false,
|
|
714
|
-
"hiddenAliases": [],
|
|
715
|
-
"id": "plugin:add",
|
|
716
|
-
"pluginAlias": "@loopress/cli",
|
|
717
|
-
"pluginName": "@loopress/cli",
|
|
718
|
-
"pluginType": "core",
|
|
719
|
-
"strict": true,
|
|
720
|
-
"enableJsonFlag": false,
|
|
721
|
-
"dryRunFlag": {
|
|
722
|
-
"dry-run": {
|
|
723
|
-
"char": "d",
|
|
724
|
-
"description": "Show what would change without making changes",
|
|
725
|
-
"allowNo": false,
|
|
726
|
-
"type": "boolean"
|
|
727
|
-
}
|
|
728
|
-
},
|
|
729
|
-
"isESM": true,
|
|
730
|
-
"relativePath": [
|
|
731
|
-
"dist",
|
|
732
|
-
"commands",
|
|
733
|
-
"plugin",
|
|
734
|
-
"add.js"
|
|
735
|
-
]
|
|
736
|
-
},
|
|
737
|
-
"plugin:pull": {
|
|
738
|
-
"aliases": [],
|
|
739
|
-
"args": {},
|
|
740
|
-
"description": "Pull installed plugins from WordPress into loopress.json",
|
|
741
|
-
"examples": [
|
|
742
|
-
"$ lps plugin pull",
|
|
743
|
-
"$ lps plugin pull --dry-run"
|
|
744
|
-
],
|
|
745
|
-
"flags": {
|
|
746
|
-
"dry-run": {
|
|
747
|
-
"char": "d",
|
|
748
|
-
"description": "Show what would change without making changes",
|
|
749
|
-
"name": "dry-run",
|
|
750
|
-
"allowNo": false,
|
|
751
|
-
"type": "boolean"
|
|
752
|
-
}
|
|
753
|
-
},
|
|
754
|
-
"hasDynamicHelp": false,
|
|
755
|
-
"hiddenAliases": [],
|
|
756
|
-
"id": "plugin:pull",
|
|
757
|
-
"pluginAlias": "@loopress/cli",
|
|
758
|
-
"pluginName": "@loopress/cli",
|
|
759
|
-
"pluginType": "core",
|
|
760
|
-
"strict": true,
|
|
761
|
-
"enableJsonFlag": false,
|
|
762
|
-
"dryRunFlag": {
|
|
763
|
-
"dry-run": {
|
|
764
|
-
"char": "d",
|
|
765
|
-
"description": "Show what would change without making changes",
|
|
766
|
-
"allowNo": false,
|
|
767
|
-
"type": "boolean"
|
|
768
|
-
}
|
|
769
|
-
},
|
|
770
|
-
"isESM": true,
|
|
771
|
-
"relativePath": [
|
|
772
|
-
"dist",
|
|
773
|
-
"commands",
|
|
774
|
-
"plugin",
|
|
775
|
-
"pull.js"
|
|
776
|
-
]
|
|
777
|
-
},
|
|
778
|
-
"plugin:push": {
|
|
779
|
-
"aliases": [],
|
|
780
|
-
"args": {},
|
|
781
|
-
"description": "Push plugins to WordPress to match loopress.json",
|
|
782
|
-
"examples": [
|
|
783
|
-
"$ lps plugin push",
|
|
784
|
-
"$ lps plugin push --dry-run"
|
|
785
|
-
],
|
|
786
|
-
"flags": {
|
|
787
|
-
"dry-run": {
|
|
788
|
-
"char": "d",
|
|
789
|
-
"description": "Show what would change without making changes",
|
|
790
|
-
"name": "dry-run",
|
|
791
|
-
"allowNo": false,
|
|
792
|
-
"type": "boolean"
|
|
793
|
-
}
|
|
794
|
-
},
|
|
795
|
-
"hasDynamicHelp": false,
|
|
796
|
-
"hiddenAliases": [],
|
|
797
|
-
"id": "plugin:push",
|
|
798
|
-
"pluginAlias": "@loopress/cli",
|
|
799
|
-
"pluginName": "@loopress/cli",
|
|
800
|
-
"pluginType": "core",
|
|
801
|
-
"strict": true,
|
|
802
|
-
"isESM": true,
|
|
803
|
-
"relativePath": [
|
|
804
|
-
"dist",
|
|
805
|
-
"commands",
|
|
806
|
-
"plugin",
|
|
807
|
-
"push.js"
|
|
808
|
-
]
|
|
809
|
-
},
|
|
810
947
|
"telemetry:disable": {
|
|
811
948
|
"aliases": [],
|
|
812
949
|
"args": {},
|
|
@@ -856,5 +993,5 @@
|
|
|
856
993
|
]
|
|
857
994
|
}
|
|
858
995
|
},
|
|
859
|
-
"version": "0.
|
|
996
|
+
"version": "0.17.0"
|
|
860
997
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopress/cli",
|
|
3
3
|
"description": "CLI tool for syncing WordPress code snippets, plugins, and Composer dependencies via the REST API",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"author": "jean-smaug",
|
|
6
6
|
"bin": {
|
|
7
7
|
"loopress": "bin/run.js",
|
|
@@ -96,6 +96,9 @@
|
|
|
96
96
|
"project": {
|
|
97
97
|
"description": "Manage WordPress project and environment configurations"
|
|
98
98
|
},
|
|
99
|
+
"seo": {
|
|
100
|
+
"description": "Manage SEO settings, post meta, and redirects (RankMath or Yoast SEO, whichever is active)"
|
|
101
|
+
},
|
|
99
102
|
"snippet": {
|
|
100
103
|
"description": "Manage WordPress code snippets"
|
|
101
104
|
},
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"description": "Directory where ACF field groups, post types, taxonomies and options pages are read from and written to, relative to rootDir.",
|
|
25
25
|
"default": "acf"
|
|
26
26
|
},
|
|
27
|
+
"seoDir": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Directory where SEO settings, post meta, and redirects (RankMath and Yoast SEO, whichever is active) are read from and written to, relative to rootDir.",
|
|
30
|
+
"default": "seo"
|
|
31
|
+
},
|
|
27
32
|
"projectId": {
|
|
28
33
|
"type": "string",
|
|
29
34
|
"description": "Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config."
|