@loopress/cli 0.15.0 → 0.16.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.16.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)
@@ -70,9 +74,109 @@ report.
70
74
  * [`lps telemetry disable`](#lps-telemetry-disable)
71
75
  * [`lps telemetry enable`](#lps-telemetry-enable)
72
76
 
77
+ ## `lps acf list`
78
+
79
+ List ACF field groups, post types, taxonomies, and options pages from WordPress
80
+
81
+ ```
82
+ USAGE
83
+ $ lps acf list [-j] [--type field-groups|post-types|taxonomies|options-pages...]
84
+
85
+ FLAGS
86
+ -j, --json Output in JSON format
87
+ --type=<option>... Limit to specific ACF object types
88
+ <options: field-groups|post-types|taxonomies|options-pages>
89
+
90
+ DESCRIPTION
91
+ List ACF field groups, post types, taxonomies, and options pages from WordPress
92
+
93
+ EXAMPLES
94
+ $ lps acf list
95
+
96
+ $ lps acf list --type field-groups
97
+ ```
98
+
99
+ _See code: [src/commands/acf/list.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/acf/list.ts)_
100
+
101
+ ## `lps acf pull [PATH]`
102
+
103
+ Pull ACF field groups, post types, taxonomies, and options pages from WordPress
104
+
105
+ ```
106
+ USAGE
107
+ $ lps acf pull [PATH] [-d] [--type field-groups|post-types|taxonomies|options-pages...]
108
+
109
+ ARGUMENTS
110
+ [PATH] Path to ACF directory (overrides project config)
111
+
112
+ FLAGS
113
+ -d, --dry-run Show what would change without making changes
114
+ --type=<option>... Limit to specific ACF object types
115
+ <options: field-groups|post-types|taxonomies|options-pages>
116
+
117
+ DESCRIPTION
118
+ Pull ACF field groups, post types, taxonomies, and options pages from WordPress
119
+
120
+ EXAMPLES
121
+ $ lps acf pull
122
+
123
+ $ lps acf pull --type field-groups
124
+ ```
125
+
126
+ _See code: [src/commands/acf/pull.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/acf/pull.ts)_
127
+
128
+ ## `lps acf push [PATH]`
129
+
130
+ Push ACF field groups, post types, taxonomies, and options pages to WordPress
131
+
132
+ ```
133
+ USAGE
134
+ $ lps acf push [PATH] [-d] [--type field-groups|post-types|taxonomies|options-pages...]
135
+
136
+ ARGUMENTS
137
+ [PATH] Path to ACF directory (overrides project config)
138
+
139
+ FLAGS
140
+ -d, --dry-run Show what would change without making changes
141
+ --type=<option>... Limit to specific ACF object types
142
+ <options: field-groups|post-types|taxonomies|options-pages>
143
+
144
+ DESCRIPTION
145
+ Push ACF field groups, post types, taxonomies, and options pages to WordPress
146
+
147
+ EXAMPLES
148
+ $ lps acf push
149
+
150
+ $ lps acf push --type field-groups
151
+ ```
152
+
153
+ _See code: [src/commands/acf/push.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/acf/push.ts)_
154
+
155
+ ## `lps composer init`
156
+
157
+ Create a composer.json wired to WPackagist for installing WordPress.org plugins and themes
158
+
159
+ ```
160
+ USAGE
161
+ $ lps composer init [-d]
162
+
163
+ FLAGS
164
+ -d, --dry-run Show what would change without making changes
165
+
166
+ DESCRIPTION
167
+ Create a composer.json wired to WPackagist for installing WordPress.org plugins and themes
168
+
169
+ EXAMPLES
170
+ $ lps composer init
171
+
172
+ $ lps composer init --dry-run
173
+ ```
174
+
175
+ _See code: [src/commands/composer/init.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/composer/init.ts)_
176
+
73
177
  ## `lps composer pull`
74
178
 
75
- Pull composer.lock from WordPress
179
+ Pull composer.json and composer.lock from WordPress
76
180
 
77
181
  ```
78
182
  USAGE
@@ -82,7 +186,7 @@ FLAGS
82
186
  -d, --dry-run Show what would change without making changes
83
187
 
84
188
  DESCRIPTION
85
- Pull composer.lock from WordPress
189
+ Pull composer.json and composer.lock from WordPress
86
190
 
87
191
  EXAMPLES
88
192
  $ lps composer pull
@@ -90,7 +194,7 @@ EXAMPLES
90
194
  $ lps composer pull --dry-run
91
195
  ```
92
196
 
93
- _See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/composer/pull.ts)_
197
+ _See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/composer/pull.ts)_
94
198
 
95
199
  ## `lps composer push`
96
200
 
@@ -112,7 +216,7 @@ EXAMPLES
112
216
  $ lps composer push --dry-run
113
217
  ```
114
218
 
115
- _See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/composer/push.ts)_
219
+ _See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/composer/push.ts)_
116
220
 
117
221
  ## `lps help [COMMAND]`
118
222
 
@@ -149,7 +253,7 @@ EXAMPLES
149
253
  $ lps init
150
254
  ```
151
255
 
152
- _See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/init.ts)_
256
+ _See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/init.ts)_
153
257
 
154
258
  ## `lps login`
155
259
 
@@ -166,7 +270,7 @@ EXAMPLES
166
270
  $ lps login
167
271
  ```
168
272
 
169
- _See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/login.ts)_
273
+ _See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/login.ts)_
170
274
 
171
275
  ## `lps logout`
172
276
 
@@ -183,7 +287,7 @@ EXAMPLES
183
287
  $ lps logout
184
288
  ```
185
289
 
186
- _See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/logout.ts)_
290
+ _See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/logout.ts)_
187
291
 
188
292
  ## `lps plugin add SLUG`
189
293
 
@@ -208,7 +312,7 @@ EXAMPLES
208
312
  $ lps plugin add contact-form-7 --dry-run
209
313
  ```
210
314
 
211
- _See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/plugin/add.ts)_
315
+ _See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/plugin/add.ts)_
212
316
 
213
317
  ## `lps plugin pull`
214
318
 
@@ -230,7 +334,7 @@ EXAMPLES
230
334
  $ lps plugin pull --dry-run
231
335
  ```
232
336
 
233
- _See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/plugin/pull.ts)_
337
+ _See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/plugin/pull.ts)_
234
338
 
235
339
  ## `lps plugin push`
236
340
 
@@ -252,7 +356,7 @@ EXAMPLES
252
356
  $ lps plugin push --dry-run
253
357
  ```
254
358
 
255
- _See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/plugin/push.ts)_
359
+ _See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/plugin/push.ts)_
256
360
 
257
361
  ## `lps project config`
258
362
 
@@ -270,7 +374,7 @@ EXAMPLES
270
374
  $ lps project config
271
375
  ```
272
376
 
273
- _See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/config.ts)_
377
+ _See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/config.ts)_
274
378
 
275
379
  ## `lps project list`
276
380
 
@@ -287,7 +391,7 @@ EXAMPLES
287
391
  $ lps project list
288
392
  ```
289
393
 
290
- _See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/list.ts)_
394
+ _See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/list.ts)_
291
395
 
292
396
  ## `lps project pull`
293
397
 
@@ -304,7 +408,7 @@ EXAMPLES
304
408
  $ lps project pull
305
409
  ```
306
410
 
307
- _See code: [src/commands/project/pull.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/pull.ts)_
411
+ _See code: [src/commands/project/pull.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/pull.ts)_
308
412
 
309
413
  ## `lps project push`
310
414
 
@@ -321,7 +425,7 @@ EXAMPLES
321
425
  $ lps project push
322
426
  ```
323
427
 
324
- _See code: [src/commands/project/push.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/push.ts)_
428
+ _See code: [src/commands/project/push.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/push.ts)_
325
429
 
326
430
  ## `lps project remove`
327
431
 
@@ -338,7 +442,7 @@ EXAMPLES
338
442
  $ lps project remove
339
443
  ```
340
444
 
341
- _See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/remove.ts)_
445
+ _See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/remove.ts)_
342
446
 
343
447
  ## `lps project switch`
344
448
 
@@ -355,7 +459,7 @@ EXAMPLES
355
459
  $ lps project switch
356
460
  ```
357
461
 
358
- _See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/project/switch.ts)_
462
+ _See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/project/switch.ts)_
359
463
 
360
464
  ## `lps snippet list`
361
465
 
@@ -375,7 +479,7 @@ EXAMPLES
375
479
  $ lps snippet list
376
480
  ```
377
481
 
378
- _See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/snippet/list.ts)_
482
+ _See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/snippet/list.ts)_
379
483
 
380
484
  ## `lps snippet publish [PATH]`
381
485
 
@@ -398,7 +502,7 @@ EXAMPLES
398
502
  $ lps snippet publish --path ./snippets
399
503
  ```
400
504
 
401
- _See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/snippet/publish.ts)_
505
+ _See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/snippet/publish.ts)_
402
506
 
403
507
  ## `lps snippet pull [PATH]`
404
508
 
@@ -423,7 +527,7 @@ EXAMPLES
423
527
  $ lps snippet pull --path ./snippets
424
528
  ```
425
529
 
426
- _See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/snippet/pull.ts)_
530
+ _See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/snippet/pull.ts)_
427
531
 
428
532
  ## `lps snippet push [PATH]`
429
533
 
@@ -449,7 +553,7 @@ EXAMPLES
449
553
  $ lps snippet push --path ./snippets
450
554
  ```
451
555
 
452
- _See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/snippet/push.ts)_
556
+ _See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/snippet/push.ts)_
453
557
 
454
558
  ## `lps status`
455
559
 
@@ -466,7 +570,7 @@ EXAMPLES
466
570
  $ lps status
467
571
  ```
468
572
 
469
- _See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/status.ts)_
573
+ _See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/status.ts)_
470
574
 
471
575
  ## `lps telemetry disable`
472
576
 
@@ -483,7 +587,7 @@ EXAMPLES
483
587
  $ lps telemetry disable
484
588
  ```
485
589
 
486
- _See code: [src/commands/telemetry/disable.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/telemetry/disable.ts)_
590
+ _See code: [src/commands/telemetry/disable.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/telemetry/disable.ts)_
487
591
 
488
592
  ## `lps telemetry enable`
489
593
 
@@ -500,5 +604,5 @@ EXAMPLES
500
604
  $ lps telemetry enable
501
605
  ```
502
606
 
503
- _See code: [src/commands/telemetry/enable.ts](https://github.com/loopress/loopress/blob/v0.15.0/src/commands/telemetry/enable.ts)_
607
+ _See code: [src/commands/telemetry/enable.ts](https://github.com/loopress/loopress/blob/v0.16.0/src/commands/telemetry/enable.ts)_
504
608
  <!-- 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
+ }
@@ -0,0 +1,47 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { existsSync } from 'node:fs';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { LoopressCommand } from '../../lib/base.js';
6
+ const WPACKAGIST_REPOSITORY = { type: 'composer', url: 'https://wpackagist.org' };
7
+ const INSTALLERS_PACKAGE = 'composer/installers';
8
+ const INSTALLERS_CONSTRAINT = '^2.0';
9
+ // The server runs Composer with `--working-dir` set to wp-content/loopress/ (see
10
+ // LoopressEnvironment::getDxDir), not the WordPress root, so installer-paths must climb out
11
+ // of that directory to land plugins/themes in their usual wp-content/ locations.
12
+ const INSTALLER_PATHS = {
13
+ '../plugins/{$name}/': ['type:wordpress-plugin'],
14
+ '../themes/{$name}/': ['type:wordpress-theme'],
15
+ };
16
+ export default class ComposerInit extends LoopressCommand {
17
+ static description = 'Create a composer.json wired to WPackagist for installing WordPress.org plugins and themes';
18
+ static examples = ['$ lps composer init', '$ lps composer init --dry-run'];
19
+ static flags = {
20
+ ...LoopressCommand.dryRunFlag,
21
+ };
22
+ async run() {
23
+ const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
24
+ if (existsSync(composerJsonPath)) {
25
+ const overwrite = await confirm({ default: false, message: 'composer.json already exists. Overwrite?' });
26
+ if (!overwrite) {
27
+ this.log('Aborted.');
28
+ return;
29
+ }
30
+ }
31
+ const composerJson = {
32
+ extra: { 'installer-paths': INSTALLER_PATHS },
33
+ name: 'loopress/site-dependencies',
34
+ repositories: [WPACKAGIST_REPOSITORY],
35
+ require: {
36
+ [INSTALLERS_PACKAGE]: INSTALLERS_CONSTRAINT,
37
+ },
38
+ };
39
+ if (this.dryRun) {
40
+ this.log(`[dry-run] Would write composer.json to ${composerJsonPath}`);
41
+ return;
42
+ }
43
+ await writeFile(composerJsonPath, JSON.stringify(composerJson, null, 2) + '\n', 'utf8');
44
+ this.log(`Wrote composer.json to ${composerJsonPath}`);
45
+ this.log('Add plugins with `wpackagist-plugin/<slug>` and a theme with `wpackagist-theme/<slug>` in require, then run `lps composer push`.');
46
+ }
47
+ }