@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 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.16.0 linux-x64 node-v24.18.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.16.0/src/commands/acf/list.ts)_
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.16.0/src/commands/acf/pull.ts)_
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.16.0/src/commands/acf/push.ts)_
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.16.0/src/commands/composer/init.ts)_
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.16.0/src/commands/composer/pull.ts)_
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.16.0/src/commands/composer/push.ts)_
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.16.0/src/commands/init.ts)_
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.16.0/src/commands/login.ts)_
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.16.0/src/commands/logout.ts)_
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.16.0/src/commands/plugin/add.ts)_
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.16.0/src/commands/plugin/pull.ts)_
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.16.0/src/commands/plugin/push.ts)_
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.16.0/src/commands/project/config.ts)_
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.16.0/src/commands/project/list.ts)_
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.16.0/src/commands/project/pull.ts)_
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.16.0/src/commands/project/push.ts)_
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.16.0/src/commands/project/remove.ts)_
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.16.0/src/commands/project/switch.ts)_
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.16.0/src/commands/snippet/list.ts)_
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.16.0/src/commands/snippet/publish.ts)_
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.16.0/src/commands/snippet/pull.ts)_
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.16.0/src/commands/snippet/push.ts)_
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.16.0/src/commands/status.ts)_
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.16.0/src/commands/telemetry/disable.ts)_
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.16.0/src/commands/telemetry/enable.ts)_
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
+ }
@@ -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
+ }
@@ -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.16.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.16.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."