@loopress/cli 0.15.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.15.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
@@ -47,6 +47,10 @@ report.
47
47
  # Commands
48
48
 
49
49
  <!-- commands -->
50
+ * [`lps acf list`](#lps-acf-list)
51
+ * [`lps acf pull [PATH]`](#lps-acf-pull-path)
52
+ * [`lps acf push [PATH]`](#lps-acf-push-path)
53
+ * [`lps composer init`](#lps-composer-init)
50
54
  * [`lps composer pull`](#lps-composer-pull)
51
55
  * [`lps composer push`](#lps-composer-push)
52
56
  * [`lps help [COMMAND]`](#lps-help-command)
@@ -62,6 +66,9 @@ report.
62
66
  * [`lps project push`](#lps-project-push)
63
67
  * [`lps project remove`](#lps-project-remove)
64
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)
65
72
  * [`lps snippet list`](#lps-snippet-list)
66
73
  * [`lps snippet publish [PATH]`](#lps-snippet-publish-path)
67
74
  * [`lps snippet pull [PATH]`](#lps-snippet-pull-path)
@@ -70,9 +77,109 @@ report.
70
77
  * [`lps telemetry disable`](#lps-telemetry-disable)
71
78
  * [`lps telemetry enable`](#lps-telemetry-enable)
72
79
 
80
+ ## `lps acf list`
81
+
82
+ List ACF field groups, post types, taxonomies, and options pages from WordPress
83
+
84
+ ```
85
+ USAGE
86
+ $ lps acf list [-j] [--type field-groups|post-types|taxonomies|options-pages...]
87
+
88
+ FLAGS
89
+ -j, --json Output in JSON format
90
+ --type=<option>... Limit to specific ACF object types
91
+ <options: field-groups|post-types|taxonomies|options-pages>
92
+
93
+ DESCRIPTION
94
+ List ACF field groups, post types, taxonomies, and options pages from WordPress
95
+
96
+ EXAMPLES
97
+ $ lps acf list
98
+
99
+ $ lps acf list --type field-groups
100
+ ```
101
+
102
+ _See code: [src/commands/acf/list.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/list.ts)_
103
+
104
+ ## `lps acf pull [PATH]`
105
+
106
+ Pull ACF field groups, post types, taxonomies, and options pages from WordPress
107
+
108
+ ```
109
+ USAGE
110
+ $ lps acf pull [PATH] [-d] [--type field-groups|post-types|taxonomies|options-pages...]
111
+
112
+ ARGUMENTS
113
+ [PATH] Path to ACF directory (overrides project config)
114
+
115
+ FLAGS
116
+ -d, --dry-run Show what would change without making changes
117
+ --type=<option>... Limit to specific ACF object types
118
+ <options: field-groups|post-types|taxonomies|options-pages>
119
+
120
+ DESCRIPTION
121
+ Pull ACF field groups, post types, taxonomies, and options pages from WordPress
122
+
123
+ EXAMPLES
124
+ $ lps acf pull
125
+
126
+ $ lps acf pull --type field-groups
127
+ ```
128
+
129
+ _See code: [src/commands/acf/pull.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/pull.ts)_
130
+
131
+ ## `lps acf push [PATH]`
132
+
133
+ Push ACF field groups, post types, taxonomies, and options pages to WordPress
134
+
135
+ ```
136
+ USAGE
137
+ $ lps acf push [PATH] [-d] [--type field-groups|post-types|taxonomies|options-pages...]
138
+
139
+ ARGUMENTS
140
+ [PATH] Path to ACF directory (overrides project config)
141
+
142
+ FLAGS
143
+ -d, --dry-run Show what would change without making changes
144
+ --type=<option>... Limit to specific ACF object types
145
+ <options: field-groups|post-types|taxonomies|options-pages>
146
+
147
+ DESCRIPTION
148
+ Push ACF field groups, post types, taxonomies, and options pages to WordPress
149
+
150
+ EXAMPLES
151
+ $ lps acf push
152
+
153
+ $ lps acf push --type field-groups
154
+ ```
155
+
156
+ _See code: [src/commands/acf/push.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/acf/push.ts)_
157
+
158
+ ## `lps composer init`
159
+
160
+ Create a composer.json wired to WPackagist for installing WordPress.org plugins and themes
161
+
162
+ ```
163
+ USAGE
164
+ $ lps composer init [-d]
165
+
166
+ FLAGS
167
+ -d, --dry-run Show what would change without making changes
168
+
169
+ DESCRIPTION
170
+ Create a composer.json wired to WPackagist for installing WordPress.org plugins and themes
171
+
172
+ EXAMPLES
173
+ $ lps composer init
174
+
175
+ $ lps composer init --dry-run
176
+ ```
177
+
178
+ _See code: [src/commands/composer/init.ts](https://github.com/loopress/loopress/blob/v0.17.0/src/commands/composer/init.ts)_
179
+
73
180
  ## `lps composer pull`
74
181
 
75
- Pull composer.lock from WordPress
182
+ Pull composer.json and composer.lock from WordPress
76
183
 
77
184
  ```
78
185
  USAGE
@@ -82,7 +189,7 @@ FLAGS
82
189
  -d, --dry-run Show what would change without making changes
83
190
 
84
191
  DESCRIPTION
85
- Pull composer.lock from WordPress
192
+ Pull composer.json and composer.lock from WordPress
86
193
 
87
194
  EXAMPLES
88
195
  $ lps composer pull
@@ -90,7 +197,7 @@ EXAMPLES
90
197
  $ lps composer pull --dry-run
91
198
  ```
92
199
 
93
- _See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
94
201
 
95
202
  ## `lps composer push`
96
203
 
@@ -112,7 +219,7 @@ EXAMPLES
112
219
  $ lps composer push --dry-run
113
220
  ```
114
221
 
115
- _See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
116
223
 
117
224
  ## `lps help [COMMAND]`
118
225
 
@@ -149,7 +256,7 @@ EXAMPLES
149
256
  $ lps init
150
257
  ```
151
258
 
152
- _See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
153
260
 
154
261
  ## `lps login`
155
262
 
@@ -166,7 +273,7 @@ EXAMPLES
166
273
  $ lps login
167
274
  ```
168
275
 
169
- _See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
170
277
 
171
278
  ## `lps logout`
172
279
 
@@ -183,7 +290,7 @@ EXAMPLES
183
290
  $ lps logout
184
291
  ```
185
292
 
186
- _See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
187
294
 
188
295
  ## `lps plugin add SLUG`
189
296
 
@@ -208,7 +315,7 @@ EXAMPLES
208
315
  $ lps plugin add contact-form-7 --dry-run
209
316
  ```
210
317
 
211
- _See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
212
319
 
213
320
  ## `lps plugin pull`
214
321
 
@@ -230,7 +337,7 @@ EXAMPLES
230
337
  $ lps plugin pull --dry-run
231
338
  ```
232
339
 
233
- _See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
234
341
 
235
342
  ## `lps plugin push`
236
343
 
@@ -252,7 +359,7 @@ EXAMPLES
252
359
  $ lps plugin push --dry-run
253
360
  ```
254
361
 
255
- _See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
256
363
 
257
364
  ## `lps project config`
258
365
 
@@ -270,7 +377,7 @@ EXAMPLES
270
377
  $ lps project config
271
378
  ```
272
379
 
273
- _See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
274
381
 
275
382
  ## `lps project list`
276
383
 
@@ -287,7 +394,7 @@ EXAMPLES
287
394
  $ lps project list
288
395
  ```
289
396
 
290
- _See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
291
398
 
292
399
  ## `lps project pull`
293
400
 
@@ -304,7 +411,7 @@ EXAMPLES
304
411
  $ lps project pull
305
412
  ```
306
413
 
307
- _See code: [src/commands/project/pull.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
308
415
 
309
416
  ## `lps project push`
310
417
 
@@ -321,7 +428,7 @@ EXAMPLES
321
428
  $ lps project push
322
429
  ```
323
430
 
324
- _See code: [src/commands/project/push.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
325
432
 
326
433
  ## `lps project remove`
327
434
 
@@ -338,7 +445,7 @@ EXAMPLES
338
445
  $ lps project remove
339
446
  ```
340
447
 
341
- _See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
342
449
 
343
450
  ## `lps project switch`
344
451
 
@@ -355,7 +462,80 @@ EXAMPLES
355
462
  $ lps project switch
356
463
  ```
357
464
 
358
- _See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
359
539
 
360
540
  ## `lps snippet list`
361
541
 
@@ -375,7 +555,7 @@ EXAMPLES
375
555
  $ lps snippet list
376
556
  ```
377
557
 
378
- _See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
379
559
 
380
560
  ## `lps snippet publish [PATH]`
381
561
 
@@ -398,7 +578,7 @@ EXAMPLES
398
578
  $ lps snippet publish --path ./snippets
399
579
  ```
400
580
 
401
- _See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
402
582
 
403
583
  ## `lps snippet pull [PATH]`
404
584
 
@@ -423,7 +603,7 @@ EXAMPLES
423
603
  $ lps snippet pull --path ./snippets
424
604
  ```
425
605
 
426
- _See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
427
607
 
428
608
  ## `lps snippet push [PATH]`
429
609
 
@@ -449,7 +629,7 @@ EXAMPLES
449
629
  $ lps snippet push --path ./snippets
450
630
  ```
451
631
 
452
- _See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
453
633
 
454
634
  ## `lps status`
455
635
 
@@ -466,7 +646,7 @@ EXAMPLES
466
646
  $ lps status
467
647
  ```
468
648
 
469
- _See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
470
650
 
471
651
  ## `lps telemetry disable`
472
652
 
@@ -483,7 +663,7 @@ EXAMPLES
483
663
  $ lps telemetry disable
484
664
  ```
485
665
 
486
- _See code: [src/commands/telemetry/disable.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
487
667
 
488
668
  ## `lps telemetry enable`
489
669
 
@@ -500,5 +680,5 @@ EXAMPLES
500
680
  $ lps telemetry enable
501
681
  ```
502
682
 
503
- _See code: [src/commands/telemetry/enable.ts](https://github.com/loopress/loopress/blob/v0.15.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)_
504
684
  <!-- commandsstop -->
@@ -0,0 +1,10 @@
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
+ type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,36 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { LoopressCommand } from '../../lib/base.js';
3
+ import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
4
+ export default class List extends LoopressCommand {
5
+ static description = 'List ACF field groups, post types, taxonomies, and options pages from WordPress';
6
+ static examples = ['$ lps acf list', '$ lps acf list --type field-groups'];
7
+ static flags = {
8
+ json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
9
+ type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
10
+ };
11
+ async run() {
12
+ const { flags } = await this.parse(List);
13
+ const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
14
+ const byType = {};
15
+ for (const type of types) {
16
+ byType[type] = await this.wp.get(acfEndpoint(type));
17
+ }
18
+ if (flags.json) {
19
+ this.log(JSON.stringify(byType, null, 2));
20
+ return;
21
+ }
22
+ for (const type of types) {
23
+ const objects = byType[type];
24
+ this.log(`${type} (${objects.length}):`);
25
+ if (objects.length === 0) {
26
+ this.log(' (none)');
27
+ this.log('');
28
+ continue;
29
+ }
30
+ for (const object of objects) {
31
+ this.log(` ${getAcfKey(object) ?? '(no key)'}. ${String(object.title ?? '(untitled)')}`);
32
+ }
33
+ this.log('');
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,15 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ export default class Pull extends LoopressCommand {
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
+ type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ private findOrphanedFiles;
14
+ private pullType;
15
+ }
@@ -0,0 +1,80 @@
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 { LoopressCommand } from '../../lib/base.js';
6
+ import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
7
+ export default class Pull extends LoopressCommand {
8
+ static args = {
9
+ path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
10
+ };
11
+ static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress';
12
+ static examples = ['$ lps acf pull', '$ lps acf pull --type field-groups'];
13
+ static flags = {
14
+ ...LoopressCommand.dryRunFlag,
15
+ type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(Pull);
19
+ const { url } = this.siteConfig;
20
+ const path = this.resolveAcfPath(args.path);
21
+ const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
22
+ this.log(`Pulling ACF configuration from ${url}`);
23
+ this.log(`ACF path: ${path}`);
24
+ for (const type of types) {
25
+ await this.pullType(type, path);
26
+ }
27
+ }
28
+ // Every file in a type's subdirectory is unambiguously `<key>.json` — unlike snippets, ACF
29
+ // objects have no numeric-id/slug filename convention. `key` is the stable identity ACF
30
+ // itself already uses for this (see its own Local JSON mechanism), so orphan detection is a
31
+ // plain set difference, no regex/extension juggling needed.
32
+ async findOrphanedFiles(dir, keepKeys) {
33
+ let files;
34
+ try {
35
+ files = await readdir(dir);
36
+ }
37
+ catch (error) {
38
+ if (error.code === 'ENOENT')
39
+ return [];
40
+ throw error;
41
+ }
42
+ return files.filter((file) => extname(file) === '.json' && !keepKeys.has(file.slice(0, -'.json'.length)));
43
+ }
44
+ async pullType(type, basePath) {
45
+ const dir = join(basePath, type);
46
+ const remoteList = await this.wp.get(acfEndpoint(type));
47
+ const withKey = remoteList.filter((object) => getAcfKey(object) !== null);
48
+ const skipped = remoteList.length - withKey.length;
49
+ const orphans = await this.findOrphanedFiles(dir, new Set(withKey.map((object) => getAcfKey(object))));
50
+ if (this.dryRun) {
51
+ this.log(`[dry-run] Would pull ${withKey.length} ${type} to ${dir}`);
52
+ if (orphans.length > 0) {
53
+ this.log(`[dry-run] Would remove ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
54
+ }
55
+ return;
56
+ }
57
+ if (withKey.length > 0)
58
+ await mkdir(dir, { recursive: true });
59
+ await new Listr(withKey.map((object) => {
60
+ const key = getAcfKey(object);
61
+ return {
62
+ async task(_ctx, task) {
63
+ await writeFile(join(dir, `${key}.json`), JSON.stringify(object, null, 2) + '\n');
64
+ task.output = `Pulled: ${key}`;
65
+ },
66
+ title: `Pull ${key}`,
67
+ };
68
+ })).run();
69
+ for (const file of orphans) {
70
+ await rm(join(dir, file), { force: true });
71
+ }
72
+ if (orphans.length > 0) {
73
+ this.warn(`Removed ${orphans.length} local file${orphans.length === 1 ? '' : 's'} in ${dir} no longer present on WordPress: ${orphans.join(', ')}`);
74
+ }
75
+ this.log(`Pulled ${withKey.length} ${type} to ${dir}`);
76
+ if (skipped > 0) {
77
+ this.warn(`${skipped} ${type} skipped because they have no key`);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,17 @@
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
+ type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ private failedCount;
13
+ run(): Promise<void>;
14
+ private loadObjects;
15
+ private pushObject;
16
+ private pushType;
17
+ }
@@ -0,0 +1,108 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { Listr } from 'listr2';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { extname, join } from 'node:path';
5
+ import { PushCommand } from '../../lib/push-command.js';
6
+ import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
7
+ export default class Push extends PushCommand {
8
+ static args = {
9
+ path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
10
+ };
11
+ static description = 'Push ACF field groups, post types, taxonomies, and options pages to WordPress';
12
+ static examples = ['$ lps acf push', '$ lps acf push --type field-groups'];
13
+ static flags = {
14
+ ...PushCommand.dryRunFlag,
15
+ type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
16
+ };
17
+ failedCount = 0;
18
+ async run() {
19
+ const { args, flags } = await this.parse(Push);
20
+ const { url } = this.siteConfig;
21
+ const path = this.resolveAcfPath(args.path);
22
+ const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
23
+ this.log(`Pushing ACF configuration to ${url}`);
24
+ this.log(`ACF path: ${path}`);
25
+ for (const type of types) {
26
+ await this.pushType(type, path);
27
+ }
28
+ if (this.failedCount > 0) {
29
+ this.error(`${this.failedCount} ACF object${this.failedCount === 1 ? '' : 's'} failed to push.`);
30
+ }
31
+ if (this.dryRun)
32
+ return;
33
+ await this.recordSuccess();
34
+ this.log('All ACF objects pushed.');
35
+ }
36
+ // One object's file is read in isolation: a corrupted or hand-broken JSON file (or one
37
+ // missing/emptying its "key") must only skip that object, not abort loading the rest of
38
+ // the type's directory — same principle as loadSnippets() for the snippet feature.
39
+ async loadObjects(dir) {
40
+ let files;
41
+ try {
42
+ files = await readdir(dir);
43
+ }
44
+ catch (error) {
45
+ if (error.code === 'ENOENT')
46
+ return [];
47
+ throw error;
48
+ }
49
+ const objects = [];
50
+ for (const file of files) {
51
+ if (extname(file) !== '.json')
52
+ continue;
53
+ const filePath = join(dir, file);
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(await readFile(filePath, 'utf8'));
57
+ }
58
+ catch (error) {
59
+ this.warn(`Skipping "${filePath}": ${error.message}`);
60
+ continue;
61
+ }
62
+ if (typeof parsed !== 'object' || parsed === null || getAcfKey(parsed) === null) {
63
+ this.warn(`Skipping "${filePath}": missing or invalid "key"`);
64
+ continue;
65
+ }
66
+ objects.push(parsed);
67
+ }
68
+ return objects;
69
+ }
70
+ // Throwing on failure (rather than returning a boolean) is what lets Listr mark the task as
71
+ // failed (red cross) instead of completed; `exitOnError: false` on the task list still lets
72
+ // sibling objects push regardless. POST alone covers create-or-update (the controller resolves
73
+ // that server-side via the object's `key`), unlike snippet push there's no numeric-id PUT/404
74
+ // fallback dance, and no rename-on-push step since `key` is permanently stable.
75
+ async pushObject(type, object, task) {
76
+ const key = getAcfKey(object) ?? '(unknown)';
77
+ if (this.dryRun) {
78
+ if (task)
79
+ task.output = `[dry-run] Would push: ${key}`;
80
+ return;
81
+ }
82
+ try {
83
+ await this.wp.post(acfEndpoint(type), object);
84
+ if (task)
85
+ task.output = `Pushed: ${key}`;
86
+ }
87
+ catch (error) {
88
+ const message = `Failed to push ${key}: ${error.message}`;
89
+ if (task)
90
+ task.output = message;
91
+ else
92
+ this.warn(` ${message}`);
93
+ this.failedCount++;
94
+ throw error;
95
+ }
96
+ }
97
+ async pushType(type, basePath) {
98
+ const dir = join(basePath, type);
99
+ const objects = await this.loadObjects(dir);
100
+ if (objects.length === 0)
101
+ return;
102
+ this.log(`Found ${objects.length} ${type} to push`);
103
+ await new Listr(objects.map((object) => ({
104
+ task: async (_ctx, task) => this.pushObject(type, object, task),
105
+ title: `Push ${getAcfKey(object) ?? '(unknown)'}`,
106
+ })), { concurrent: false, exitOnError: false }).run();
107
+ }
108
+ }
@@ -0,0 +1,9 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ export default class ComposerInit extends LoopressCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }