@loopress/cli 0.32.1 → 0.33.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.
Files changed (149) hide show
  1. package/README.md +511 -154
  2. package/dist/commands/acf/list.d.ts +1 -1
  3. package/dist/commands/acf/list.js +3 -4
  4. package/dist/commands/acf/pull.d.ts +1 -1
  5. package/dist/commands/acf/pull.js +9 -6
  6. package/dist/commands/acf/push.d.ts +1 -1
  7. package/dist/commands/acf/push.js +5 -4
  8. package/dist/commands/api/publish.js +2 -1
  9. package/dist/commands/app/pull.js +2 -1
  10. package/dist/commands/app/push.js +5 -4
  11. package/dist/commands/app/{remove.d.ts → rm.d.ts} +1 -0
  12. package/dist/commands/app/{remove.js → rm.js} +2 -1
  13. package/dist/commands/block/diff.d.ts +2 -0
  14. package/dist/commands/block/diff.js +5 -0
  15. package/dist/commands/block/list.d.ts +8 -0
  16. package/dist/commands/block/list.js +23 -0
  17. package/dist/commands/block/pull.d.ts +21 -0
  18. package/dist/commands/block/pull.js +67 -0
  19. package/dist/commands/block/push.d.ts +20 -0
  20. package/dist/commands/block/push.js +48 -0
  21. package/dist/commands/block/rm.d.ts +23 -0
  22. package/dist/commands/block/rm.js +69 -0
  23. package/dist/commands/block/rollback.d.ts +2 -0
  24. package/dist/commands/block/rollback.js +5 -0
  25. package/dist/commands/composer/init.js +1 -0
  26. package/dist/commands/composer/push.js +5 -4
  27. package/dist/commands/content/copy.d.ts +3 -0
  28. package/dist/commands/content/copy.js +79 -2
  29. package/dist/commands/dev.js +4 -3
  30. package/dist/commands/diff.d.ts +1 -0
  31. package/dist/commands/diff.js +33 -3
  32. package/dist/commands/doctor.d.ts +4 -2
  33. package/dist/commands/doctor.js +37 -23
  34. package/dist/commands/form/pull.js +7 -5
  35. package/dist/commands/form/push.js +4 -3
  36. package/dist/commands/init.js +4 -3
  37. package/dist/commands/menu/push.js +8 -8
  38. package/dist/commands/option/list.js +5 -2
  39. package/dist/commands/option/push.js +2 -1
  40. package/dist/commands/option/{remove.d.ts → rm.d.ts} +1 -0
  41. package/dist/commands/option/{remove.js → rm.js} +2 -1
  42. package/dist/commands/page/push.js +2 -1
  43. package/dist/commands/plugin/audit.d.ts +4 -4
  44. package/dist/commands/plugin/audit.js +35 -15
  45. package/dist/commands/plugin/pull.js +3 -2
  46. package/dist/commands/plugin/push.js +2 -2
  47. package/dist/commands/plugin/status.js +3 -1
  48. package/dist/commands/project/config.js +7 -6
  49. package/dist/commands/project/pull.js +3 -1
  50. package/dist/commands/project/push.js +6 -4
  51. package/dist/commands/project/{remove.d.ts → rm.d.ts} +1 -0
  52. package/dist/commands/project/{remove.js → rm.js} +3 -2
  53. package/dist/commands/project/switch.js +6 -4
  54. package/dist/commands/promote.js +7 -2
  55. package/dist/commands/pull.d.ts +2 -0
  56. package/dist/commands/pull.js +42 -18
  57. package/dist/commands/push.d.ts +2 -1
  58. package/dist/commands/push.js +69 -25
  59. package/dist/commands/snippet/publish.js +9 -5
  60. package/dist/commands/snippet/push.d.ts +1 -1
  61. package/dist/commands/snippet/push.js +15 -12
  62. package/dist/commands/status.js +1 -0
  63. package/dist/commands/theme/activate.d.ts +21 -0
  64. package/dist/commands/theme/activate.js +44 -0
  65. package/dist/commands/theme/pull.js +2 -1
  66. package/dist/commands/theme/push.js +7 -3
  67. package/dist/commands/theme/style/push.js +7 -2
  68. package/dist/commands/theme/template/push.js +25 -6
  69. package/dist/config/auth.manager.d.ts +10 -7
  70. package/dist/config/auth.manager.js +5 -1
  71. package/dist/config/json-file.d.ts +1 -1
  72. package/dist/config/project-config.manager.d.ts +2 -2
  73. package/dist/config/project-config.manager.js +13 -21
  74. package/dist/lib/api-client.js +16 -21
  75. package/dist/lib/app-build.js +6 -2
  76. package/dist/lib/app-manifest.js +6 -6
  77. package/dist/lib/base.d.ts +2 -1
  78. package/dist/lib/base.js +10 -8
  79. package/dist/lib/bootstrap-full-install.js +8 -6
  80. package/dist/lib/capabilities.d.ts +44 -0
  81. package/dist/lib/capabilities.js +85 -0
  82. package/dist/lib/declared-type-command.js +4 -2
  83. package/dist/lib/dev-watch.js +5 -5
  84. package/dist/lib/diff-command.d.ts +2 -3
  85. package/dist/lib/diff-command.js +9 -7
  86. package/dist/lib/diff-state.js +2 -3
  87. package/dist/lib/load-files.js +2 -1
  88. package/dist/lib/load-snippets.js +37 -42
  89. package/dist/lib/local-callback-server.js +7 -1
  90. package/dist/lib/narrow.d.ts +5 -0
  91. package/dist/lib/narrow.js +26 -0
  92. package/dist/lib/php-files-command.js +8 -5
  93. package/dist/lib/plugin-detection.d.ts +0 -1
  94. package/dist/lib/plugin-detection.js +2 -4
  95. package/dist/lib/push-command.d.ts +1 -1
  96. package/dist/lib/push-command.js +19 -7
  97. package/dist/lib/put-or-create.d.ts +2 -2
  98. package/dist/lib/readdir-tolerant.js +3 -1
  99. package/dist/lib/resource-state.d.ts +1 -1
  100. package/dist/lib/resource-state.js +58 -19
  101. package/dist/lib/rollback-command.d.ts +1 -1
  102. package/dist/lib/rollback-command.js +3 -2
  103. package/dist/lib/rollback-materialize.js +28 -8
  104. package/dist/lib/rotate-app-password.js +1 -0
  105. package/dist/lib/sentry.js +2 -2
  106. package/dist/lib/snapshot-store.d.ts +10 -7
  107. package/dist/lib/snapshot-store.js +21 -5
  108. package/dist/lib/spinner.js +4 -0
  109. package/dist/lib/validate-local.js +35 -20
  110. package/dist/lib/wp-client.d.ts +6 -1
  111. package/dist/lib/wp-client.js +54 -69
  112. package/dist/lib/wp-site-diagnostic.js +3 -5
  113. package/dist/types/global-config.generated.d.ts +1 -1
  114. package/dist/types/plugin.d.ts +7 -5
  115. package/dist/types/plugin.js +6 -1
  116. package/dist/types/project-config.generated.d.ts +9 -1
  117. package/dist/types/snippet.d.ts +2 -2
  118. package/dist/types/snippet.js +1 -1
  119. package/dist/utils/acf-format.d.ts +1 -0
  120. package/dist/utils/acf-format.js +11 -3
  121. package/dist/utils/block-format.d.ts +27 -0
  122. package/dist/utils/block-format.js +93 -0
  123. package/dist/utils/composer.d.ts +7 -15
  124. package/dist/utils/composer.js +43 -16
  125. package/dist/utils/declared-type-format.js +26 -19
  126. package/dist/utils/form-format.js +2 -2
  127. package/dist/utils/loopress-config.js +9 -1
  128. package/dist/utils/menu-format.d.ts +3 -1
  129. package/dist/utils/menu-format.js +5 -0
  130. package/dist/utils/option-format.d.ts +2 -2
  131. package/dist/utils/option-format.js +15 -25
  132. package/dist/utils/page-format.js +7 -6
  133. package/dist/utils/php-top-level.js +15 -4
  134. package/dist/utils/plugin-sync.d.ts +7 -7
  135. package/dist/utils/plugin-sync.js +11 -11
  136. package/dist/utils/plugins.js +9 -6
  137. package/dist/utils/resource-dirs.d.ts +2 -0
  138. package/dist/utils/resource-dirs.js +9 -1
  139. package/dist/utils/snippet-format.d.ts +1 -1
  140. package/dist/utils/snippet-format.js +5 -3
  141. package/dist/utils/template-format.js +2 -2
  142. package/dist/utils/theme-css.js +14 -6
  143. package/dist/utils/theme-styles-format.d.ts +3 -0
  144. package/dist/utils/theme-styles-format.js +44 -6
  145. package/dist/utils/version.js +1 -1
  146. package/oclif.manifest.json +1588 -1043
  147. package/package.json +42 -17
  148. package/schemas/global-config.schema.json +1 -1
  149. package/schemas/project-config.schema.json +11 -1
@@ -5,7 +5,7 @@ export default class List extends LoopressCommand {
5
5
  static enableJsonFlag: boolean;
6
6
  static examples: string[];
7
7
  static flags: {
8
- type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ type: import("@oclif/core/interfaces").OptionFlag<("taxonomies" | "field-groups" | "post-types" | "options-pages")[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  };
10
10
  run(): Promise<Record<string, Array<Record<string, unknown>>>>;
11
11
  }
@@ -7,17 +7,16 @@ export default class List extends LoopressCommand {
7
7
  static enableJsonFlag = true;
8
8
  static examples = ['$ lps acf list', '$ lps acf list --type field-groups'];
9
9
  static flags = {
10
- type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
10
+ type: Flags.option({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES })(),
11
11
  };
12
12
  async run() {
13
13
  const { flags } = await this.parse(List);
14
- const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
14
+ const types = flags.type && flags.type.length > 0 ? flags.type : [...ACF_OBJECT_TYPES];
15
15
  const byType = {};
16
16
  for (const type of types) {
17
17
  byType[type] = await this.wp.get(acfEndpoint(type));
18
18
  }
19
- for (const type of types) {
20
- const objects = byType[type];
19
+ for (const [type, objects] of Object.entries(byType)) {
21
20
  this.log(`${type} (${objects.length}):`);
22
21
  if (objects.length === 0) {
23
22
  this.log(' (none)');
@@ -14,7 +14,7 @@ export default class Pull extends LoopressCommand {
14
14
  static enableJsonFlag: boolean;
15
15
  static examples: string[];
16
16
  static flags: {
17
- type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ type: import("@oclif/core/interfaces").OptionFlag<("taxonomies" | "field-groups" | "post-types" | "options-pages")[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
18
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
19
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  };
@@ -15,13 +15,13 @@ export default class Pull extends LoopressCommand {
15
15
  static flags = {
16
16
  ...LoopressCommand.dryRunFlag,
17
17
  ...LoopressCommand.yesFlag,
18
- type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
18
+ type: Flags.option({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES })(),
19
19
  };
20
20
  async run() {
21
21
  const { args, flags } = await this.parse(Pull);
22
22
  const { url } = this.siteConfig;
23
23
  const path = this.resolveAcfPath(args.path);
24
- const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
24
+ const types = flags.type && flags.type.length > 0 ? flags.type : [...ACF_OBJECT_TYPES];
25
25
  this.log(`Pulling ACF configuration from ${url}`);
26
26
  this.log(`ACF path: ${path}`);
27
27
  for (const type of types) {
@@ -32,12 +32,15 @@ export default class Pull extends LoopressCommand {
32
32
  async pullType(type, basePath) {
33
33
  const dir = join(basePath, type);
34
34
  const remoteList = await this.wp.get(acfEndpoint(type));
35
- const withKey = remoteList.filter((object) => getAcfKey(object) !== null);
35
+ const withKey = remoteList.flatMap((object) => {
36
+ const key = getAcfKey(object);
37
+ return key === null ? [] : [{ key, object }];
38
+ });
36
39
  const skipped = remoteList.length - withKey.length;
37
40
  // Every file in a type's subdirectory is unambiguously `<key>.json`: `key` is the stable
38
41
  // identity ACF itself already uses (see its own Local JSON mechanism), no numeric-id/slug
39
42
  // filename convention like snippets.
40
- const orphans = await findOrphanedFiles(dir, new Set(withKey.map((object) => getAcfKey(object))), {
43
+ const orphans = await findOrphanedFiles(dir, new Set(withKey.map(({ key }) => key)), {
41
44
  extensions: ['.json'],
42
45
  key: basenameKey,
43
46
  });
@@ -45,8 +48,8 @@ export default class Pull extends LoopressCommand {
45
48
  dryRunMessage: `Would pull ${withKey.length} ${type} to ${dir}`,
46
49
  orphanReason: `in ${dir} no longer present on WordPress`,
47
50
  pulledMessage: `Pulled ${withKey.length} ${type} to ${dir}`,
48
- title: (object) => getAcfKey(object),
49
- write: async (object, writeDir) => writeFile(join(writeDir, `${getAcfKey(object)}.json`), JSON.stringify(object, null, 2) + '\n'),
51
+ title: ({ key }) => key,
52
+ write: async ({ key, object }, writeDir) => writeFile(join(writeDir, `${key}.json`), JSON.stringify(object, null, 2) + '\n'),
50
53
  });
51
54
  if (this.dryRun)
52
55
  return;
@@ -18,7 +18,7 @@ export default class Push extends PushCommand {
18
18
  static enableJsonFlag: boolean;
19
19
  static examples: string[];
20
20
  static flags: {
21
- type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ type: import("@oclif/core/interfaces").OptionFlag<("taxonomies" | "field-groups" | "post-types" | "options-pages")[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
22
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
23
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
24
  };
@@ -1,6 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { join } from 'node:path';
3
3
  import { loadFiles } from '../../lib/load-files.js';
4
+ import { errorMessage, isRecord } from '../../lib/narrow.js';
4
5
  import { PushCommand } from '../../lib/push-command.js';
5
6
  import { getResourceStateProvider } from '../../lib/resource-state.js';
6
7
  import { isNotFoundError } from '../../lib/wp-client.js';
@@ -17,13 +18,13 @@ export default class Push extends PushCommand {
17
18
  static flags = {
18
19
  ...PushCommand.dryRunFlag,
19
20
  ...PushCommand.yesFlag,
20
- type: Flags.string({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES }),
21
+ type: Flags.option({ description: 'Limit to specific ACF object types', multiple: true, options: ACF_OBJECT_TYPES })(),
21
22
  };
22
23
  async run() {
23
24
  const { args, flags } = await this.parse(Push);
24
25
  const { url } = this.siteConfig;
25
26
  const path = this.resolveAcfPath(args.path);
26
- const types = (flags.type && flags.type.length > 0 ? flags.type : ACF_OBJECT_TYPES);
27
+ const types = flags.type && flags.type.length > 0 ? flags.type : [...ACF_OBJECT_TYPES];
27
28
  this.log(`Pushing ACF configuration to ${url}`);
28
29
  this.log(`ACF path: ${path}`);
29
30
  const provider = getResourceStateProvider('acf');
@@ -66,7 +67,7 @@ export default class Push extends PushCommand {
66
67
  onSkip: (message) => { this.warn(message); },
67
68
  parse(raw) {
68
69
  const parsed = JSON.parse(raw);
69
- if (typeof parsed !== 'object' || parsed === null || getAcfKey(parsed) === null) {
70
+ if (!isRecord(parsed) || getAcfKey(parsed) === null) {
70
71
  throw new Error('missing or invalid "key"');
71
72
  }
72
73
  return parsed;
@@ -96,7 +97,7 @@ export default class Push extends PushCommand {
96
97
  task.output = `Pushed: ${key}`;
97
98
  }
98
99
  catch (error) {
99
- this.reportTaskFailure(`Failed to push ${key}: ${error.message}`, error, task);
100
+ this.reportTaskFailure(`Failed to push ${key}: ${errorMessage(error)}`, error, task);
100
101
  }
101
102
  }
102
103
  async pushType(type, basePath) {
@@ -2,6 +2,7 @@ import { Args, Command } from '@oclif/core';
2
2
  import { basename, join } from 'node:path';
3
3
  import { ApiClient } from '../../lib/api-client.js';
4
4
  import { loadFiles } from '../../lib/load-files.js';
5
+ import { errorMessage } from '../../lib/narrow.js';
5
6
  import { resolveLinkedProject } from '../../lib/resolve-linked-project.js';
6
7
  import { pluralize } from '../../utils/pluralize.js';
7
8
  // Publishes to the Loopress api (not a WordPress site), so this does not extend
@@ -37,7 +38,7 @@ export default class Publish extends Command {
37
38
  });
38
39
  }
39
40
  catch (error) {
40
- this.error(error.message);
41
+ this.error(errorMessage(error));
41
42
  }
42
43
  this.log(`Published ${pluralize(files.length, 'route')} to your Loopress account.`);
43
44
  }
@@ -7,6 +7,7 @@ import { dirname, join } from 'node:path';
7
7
  import { APP_CONFIG_FILENAME, parseAppConfig } from '../../lib/app-manifest.js';
8
8
  import { LoopressCommand } from '../../lib/base.js';
9
9
  import { isInteractive } from '../../lib/interactive.js';
10
+ import { errorCode } from '../../lib/narrow.js';
10
11
  import { pluralize } from '../../utils/pluralize.js';
11
12
  export default class Pull extends LoopressCommand {
12
13
  static args = {
@@ -49,7 +50,7 @@ export default class Pull extends LoopressCommand {
49
50
  entries = await readdir(path, { withFileTypes: true });
50
51
  }
51
52
  catch (error) {
52
- if (error.code === 'ENOENT')
53
+ if (errorCode(error) === 'ENOENT')
53
54
  return [];
54
55
  throw error;
55
56
  }
@@ -4,6 +4,7 @@ import { readdir, readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { buildApp } from '../../lib/app-build.js';
6
6
  import { APP_CONFIG_FILENAME, diffFiles, loadAppManifest, } from '../../lib/app-manifest.js';
7
+ import { errorCode, errorMessage } from '../../lib/narrow.js';
7
8
  import { PushCommand } from '../../lib/push-command.js';
8
9
  import { isNotFoundError } from '../../lib/wp-client.js';
9
10
  import { pluralize } from '../../utils/pluralize.js';
@@ -54,7 +55,7 @@ export default class Push extends PushCommand {
54
55
  entries = await readdir(path, { withFileTypes: true });
55
56
  }
56
57
  catch (error) {
57
- if (error.code === 'ENOENT') {
58
+ if (errorCode(error) === 'ENOENT') {
58
59
  this.error(`Apps directory not found: ${path}. Create ${join(path, '<name>', APP_CONFIG_FILENAME)} first.`);
59
60
  }
60
61
  throw error;
@@ -90,7 +91,7 @@ export default class Push extends PushCommand {
90
91
  ({ distDir, manifest } = await loadAppManifest(appDir, dirName));
91
92
  }
92
93
  catch (error) {
93
- this.reportTaskFailure(`${dirName}: ${error.message}`, error, task);
94
+ this.reportTaskFailure(`${dirName}: ${errorMessage(error)}`, error, task);
94
95
  }
95
96
  const remote = await this.remoteFiles(manifest.name);
96
97
  const changed = diffFiles(manifest.files, remote);
@@ -113,7 +114,7 @@ export default class Push extends PushCommand {
113
114
  committed = await this.wp.post(`loopress/v1/apps/${manifest.name}/commit`, manifest);
114
115
  }
115
116
  catch (error) {
116
- this.reportTaskFailure(`${manifest.name}: commit failed: ${error.message}`, error, task);
117
+ this.reportTaskFailure(`${manifest.name}: commit failed: ${errorMessage(error)}`, error, task);
117
118
  }
118
119
  this.warnIfCrossOriginIgnored(manifest, committed);
119
120
  if (task) {
@@ -141,7 +142,7 @@ export default class Push extends PushCommand {
141
142
  await this.wp.put(`loopress/v1/apps/${name}/assets`, { content, encoding: 'base64', path: file.path });
142
143
  }
143
144
  catch (error) {
144
- this.reportTaskFailure(`${name}: failed to upload ${file.path}: ${error.message}`, error);
145
+ this.reportTaskFailure(`${name}: failed to upload ${file.path}: ${errorMessage(error)}`, error);
145
146
  }
146
147
  }
147
148
  // An older plugin, or an app .htaccess the site owner manages, commits the build without the
@@ -5,6 +5,7 @@ type RemoveResult = {
5
5
  status: 'aborted' | 'dry-run' | 'success';
6
6
  };
7
7
  export default class Remove extends LoopressCommand {
8
+ static aliases: string[];
8
9
  static args: {
9
10
  name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
10
11
  };
@@ -4,12 +4,13 @@ import { LoopressCommand } from '../../lib/base.js';
4
4
  import { guardProductionPush } from '../../lib/guard-production-push.js';
5
5
  import { isInteractive } from '../../lib/interactive.js';
6
6
  export default class Remove extends LoopressCommand {
7
+ static aliases = ['app:remove'];
7
8
  static args = {
8
9
  name: Args.string({ description: 'App to remove from WordPress', required: true }),
9
10
  };
10
11
  static description = 'Remove a single-page app from WordPress: deletes its bundle from wp-content/loopress/apps/ and unregisters the shortcode. Local files are left untouched.';
11
12
  static enableJsonFlag = true;
12
- static examples = ['$ lps app remove search'];
13
+ static examples = ['$ lps app rm search'];
13
14
  static flags = {
14
15
  ...LoopressCommand.dryRunFlag,
15
16
  ...LoopressCommand.yesFlag,
@@ -0,0 +1,2 @@
1
+ declare const _default: typeof import("../../lib/diff-command.js").DiffCommand;
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { resourceDiffCommand } from '../../lib/diff-command.js';
2
+ export default resourceDiffCommand('block', {
3
+ description: 'Show what differs in dynamic blocks between your local blocks/ folders and a WordPress environment, or between two environments',
4
+ pathNoun: 'blocks directory',
5
+ });
@@ -0,0 +1,8 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ import { type RemoteBlock } from '../../utils/block-format.js';
3
+ export default class List extends LoopressCommand {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ run(): Promise<RemoteBlock[]>;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ import { BLOCKS_ENDPOINT } from '../../utils/block-format.js';
3
+ import { pluralize } from '../../utils/pluralize.js';
4
+ export default class List extends LoopressCommand {
5
+ static description = 'List the dynamic blocks Loopress registers on WordPress (loopress/<slug>), with their load and render errors';
6
+ static enableJsonFlag = true;
7
+ static examples = ['$ lps block list'];
8
+ async run() {
9
+ const blocks = await this.wp.get(BLOCKS_ENDPOINT);
10
+ if (blocks.length === 0) {
11
+ this.log('No dynamic blocks found');
12
+ return blocks;
13
+ }
14
+ this.log(`Found ${pluralize(blocks.length, 'block')}:`);
15
+ for (const block of blocks) {
16
+ const notRegistered = block.error ? ` not registered: ${block.error}` : '';
17
+ this.log(` loopress/${block.slug} (${Object.keys(block.files).join(', ')})${notRegistered}`);
18
+ if (block.runtimeError)
19
+ this.log(` render.php threw, last at ${block.runtimeError.at}: ${block.runtimeError.message}`);
20
+ }
21
+ return blocks;
22
+ }
23
+ }
@@ -0,0 +1,21 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ type PullResult = {
3
+ orphans: string[];
4
+ pulled: string[];
5
+ status: 'dry-run' | 'success';
6
+ };
7
+ export default class Pull extends LoopressCommand {
8
+ static args: {
9
+ path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
10
+ };
11
+ static description: string;
12
+ static enableJsonFlag: boolean;
13
+ static examples: string[];
14
+ static flags: {
15
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<PullResult>;
19
+ private removeOrphanBlocks;
20
+ }
21
+ export {};
@@ -0,0 +1,67 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Args } from '@oclif/core';
3
+ import { rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { LoopressCommand } from '../../lib/base.js';
6
+ import { isInteractive } from '../../lib/interactive.js';
7
+ import { errorMessage } from '../../lib/narrow.js';
8
+ import { assertSafeBlock, BLOCKS_ENDPOINT, readLocalBlocks, writeBlock } from '../../utils/block-format.js';
9
+ import { pluralize } from '../../utils/pluralize.js';
10
+ import { resolveResourceDir } from '../../utils/resource-dirs.js';
11
+ export default class Pull extends LoopressCommand {
12
+ static args = {
13
+ path: Args.string({ description: 'Path to blocks directory (overrides project config)' }),
14
+ };
15
+ static description = 'Pull the dynamic blocks Loopress registers on WordPress into blocks/<slug>/ folders';
16
+ static enableJsonFlag = true;
17
+ static examples = ['$ lps block pull'];
18
+ static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
19
+ async run() {
20
+ const { args } = await this.parse(Pull);
21
+ const path = resolveResourceDir('block', this.localConfig, args.path);
22
+ this.log(`Pulling dynamic blocks from ${this.siteConfig.url}`);
23
+ this.log(`Blocks path: ${path}`);
24
+ const remote = await this.wp.get(BLOCKS_ENDPOINT);
25
+ // All of them before the first rm() below, so a bad entry never costs a good local block.
26
+ try {
27
+ for (const block of remote)
28
+ assertSafeBlock(block);
29
+ }
30
+ catch (error) {
31
+ this.error(errorMessage(error));
32
+ }
33
+ const remoteSlugs = new Set(remote.map((block) => block.slug));
34
+ const orphans = (await readLocalBlocks(path)).blocks.map((block) => block.slug).filter((slug) => !remoteSlugs.has(slug));
35
+ const pulled = remote.map((block) => block.slug);
36
+ if (this.dryRun) {
37
+ this.log(`[dry-run] Would pull ${pluralize(remote.length, 'block')} to ${path}`);
38
+ if (orphans.length > 0)
39
+ this.log(`[dry-run] Would remove ${pluralize(orphans.length, 'local block')} no longer on WordPress: ${orphans.join(', ')}`);
40
+ return { orphans, pulled, status: 'dry-run' };
41
+ }
42
+ for (const block of remote) {
43
+ // Replaced as a whole, like on the site: a file dropped there is dropped here too.
44
+ await rm(join(path, block.slug), { force: true, recursive: true });
45
+ await writeBlock(path, { files: block.files, slug: block.slug });
46
+ }
47
+ await this.removeOrphanBlocks(path, orphans);
48
+ this.log(`Pulled ${pluralize(remote.length, 'block')} to ${path}`);
49
+ return { orphans, pulled, status: 'success' };
50
+ }
51
+ // Same confirmation rules as removeOrphanedFiles, for folders.
52
+ async removeOrphanBlocks(path, orphans) {
53
+ if (orphans.length === 0)
54
+ return;
55
+ const description = `${pluralize(orphans.length, 'local block')} no longer on WordPress: ${orphans.join(', ')}`;
56
+ if (!this.yes && isInteractive() && !(await confirm({ default: true, message: `Remove ${description}?` }))) {
57
+ this.log(`Kept ${description}`);
58
+ return;
59
+ }
60
+ for (const slug of orphans)
61
+ await rm(join(path, slug), { force: true, recursive: true });
62
+ if (this.yes || isInteractive())
63
+ this.log(`Removed ${description}`);
64
+ else
65
+ this.warn(`Removed ${description}`);
66
+ }
67
+ }
@@ -0,0 +1,20 @@
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
+ type PushResult = {
3
+ changes?: PushChange[];
4
+ pushed: string[];
5
+ status: 'dry-run' | 'success';
6
+ };
7
+ export default class Push extends PushCommand {
8
+ static args: {
9
+ path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
10
+ };
11
+ static description: string;
12
+ static enableJsonFlag: boolean;
13
+ static examples: string[];
14
+ static flags: {
15
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<PushResult>;
19
+ }
20
+ export {};
@@ -0,0 +1,48 @@
1
+ import { Args } from '@oclif/core';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { errorMessage } from '../../lib/narrow.js';
4
+ import { PushCommand } from '../../lib/push-command.js';
5
+ import { getResourceStateProvider } from '../../lib/resource-state.js';
6
+ import { BLOCKS_ENDPOINT, formatBlockProblems, readLocalBlocks } from '../../utils/block-format.js';
7
+ import { pluralize } from '../../utils/pluralize.js';
8
+ import { resolveResourceDir } from '../../utils/resource-dirs.js';
9
+ export default class Push extends PushCommand {
10
+ static args = {
11
+ path: Args.string({ description: 'Path to blocks directory (overrides project config)' }),
12
+ };
13
+ static description = 'Push local dynamic blocks (blocks/<slug>/ holding block.json, render.php, and optionally style.css and view.js) to WordPress, ' +
14
+ 'registered as loopress/<slug> and placed with <!-- wp:loopress/<slug> /-->. Each block is replaced as a whole. Create or update only, never deletes a block (use `lps block rm`).';
15
+ static enableJsonFlag = true;
16
+ static examples = ['$ lps block push', '$ lps block push --dry-run'];
17
+ static flags = { ...PushCommand.dryRunFlag, ...PushCommand.yesFlag };
18
+ async run() {
19
+ const { args } = await this.parse(Push);
20
+ const path = resolveResourceDir('block', this.localConfig, args.path);
21
+ this.log(`Pushing dynamic blocks to ${this.siteConfig.url}`);
22
+ this.log(`Blocks path: ${path}`);
23
+ const { blocks, problems } = await readLocalBlocks(path);
24
+ if (problems.length > 0)
25
+ this.error(formatBlockProblems(problems));
26
+ this.log(`Found ${pluralize(blocks.length, 'block')} to push`);
27
+ const provider = getResourceStateProvider('block');
28
+ const beforeState = await this.captureBeforePushState(provider, path);
29
+ if (this.dryRun)
30
+ return { changes: this.previewChanges, pushed: blocks.map((block) => block.slug), status: 'dry-run' };
31
+ // Unchanged blocks are skipped: each one is a write of its whole folder.
32
+ const changed = blocks.filter((block) => !isDeepStrictEqual(beforeState?.get(block.slug), block.files));
33
+ await this.runPushTasks(changed, (block) => `loopress/${block.slug}`, async (block, task) => {
34
+ try {
35
+ await this.wp.put(BLOCKS_ENDPOINT, { files: block.files, slug: block.slug });
36
+ }
37
+ catch (error) {
38
+ this.reportTaskFailure(`${block.slug}: ${errorMessage(error)}`, error, task);
39
+ }
40
+ });
41
+ if (this.failedCount > 0)
42
+ this.error(`${pluralize(this.failedCount, 'block')} failed to push.`);
43
+ await this.writeAfterPushSnapshot(provider, path, beforeState);
44
+ await this.recordSuccess();
45
+ this.log(changed.length === 0 ? 'All blocks already up to date.' : 'All blocks pushed.');
46
+ return { pushed: changed.map((block) => block.slug), status: 'success' };
47
+ }
48
+ }
@@ -0,0 +1,23 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ type RmResult = {
3
+ references: string[];
4
+ removed: boolean;
5
+ slug: string;
6
+ status: 'aborted' | 'dry-run' | 'success';
7
+ };
8
+ export default class Rm extends LoopressCommand {
9
+ static aliases: string[];
10
+ static args: {
11
+ slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static description: string;
14
+ static enableJsonFlag: boolean;
15
+ static examples: string[];
16
+ static flags: {
17
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ };
20
+ run(): Promise<RmResult>;
21
+ private localReferences;
22
+ }
23
+ export {};
@@ -0,0 +1,69 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Args } from '@oclif/core';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join, relative } from 'node:path';
5
+ import { LoopressCommand } from '../../lib/base.js';
6
+ import { isInteractive } from '../../lib/interactive.js';
7
+ import { readdirTolerant } from '../../lib/readdir-tolerant.js';
8
+ import { isApplicative404 } from '../../lib/wp-client.js';
9
+ import { BLOCK_SLUG, BLOCKS_ENDPOINT } from '../../utils/block-format.js';
10
+ import { resolveResourceDir } from '../../utils/resource-dirs.js';
11
+ export default class Rm extends LoopressCommand {
12
+ static aliases = ['block:remove'];
13
+ static args = {
14
+ slug: Args.string({ description: 'The block to remove, its blocks/ folder name (e.g. "hero")', required: true }),
15
+ };
16
+ static description = 'Remove a dynamic block from WordPress. A <!-- wp:loopress/<slug> /--> left in a page or template renders nothing until it is pushed again. Local files are left untouched.';
17
+ static enableJsonFlag = true;
18
+ static examples = ['$ lps block rm hero', '$ lps block rm hero --yes'];
19
+ static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
20
+ async run() {
21
+ const { args } = await this.parse(Rm);
22
+ const { slug } = args;
23
+ const { url } = this.siteConfig;
24
+ if (!BLOCK_SLUG.test(slug))
25
+ this.error(`Invalid block slug "${slug}": lowercase letters, digits and hyphens, starting with a letter.`);
26
+ const references = await this.localReferences(slug);
27
+ if (references.length > 0) {
28
+ this.warn(`loopress/${slug} is still used in ${references.join(', ')}: it will render nothing there.`);
29
+ }
30
+ if (this.dryRun) {
31
+ this.log(`[dry-run] Would remove loopress/${slug} from ${url}`);
32
+ return { references, removed: false, slug, status: 'dry-run' };
33
+ }
34
+ if (!this.yes) {
35
+ if (!isInteractive())
36
+ this.error(`Removing loopress/${slug} needs confirmation. Re-run with --yes.`);
37
+ if (!(await confirm({ default: false, message: `Remove loopress/${slug} from ${url}?` }))) {
38
+ this.log('Aborted.');
39
+ return { references, removed: false, slug, status: 'aborted' };
40
+ }
41
+ }
42
+ try {
43
+ await this.wp.delete(`${BLOCKS_ENDPOINT}?slug=${encodeURIComponent(slug)}`);
44
+ }
45
+ catch (error) {
46
+ if (isApplicative404(error, `Unknown block "${slug}"`))
47
+ this.error(`loopress/${slug} is not on ${url}.`);
48
+ throw error;
49
+ }
50
+ this.log(`Removed loopress/${slug} from ${url}`);
51
+ return { references, removed: true, slug, status: 'success' };
52
+ }
53
+ // The project's own files still placing the block: pages, templates, parts and patterns.
54
+ async localReferences(slug) {
55
+ const marker = new RegExp(String.raw `<!--\s*wp:loopress/${slug}[\s/]`);
56
+ const found = [];
57
+ for (const kind of ['page', 'template', 'part', 'pattern']) {
58
+ const dir = resolveResourceDir(kind, this.localConfig);
59
+ for (const entry of await readdirTolerant(dir, { recursive: true, withFileTypes: true })) {
60
+ if (!entry.isFile() || !entry.name.endsWith('.html'))
61
+ continue;
62
+ const file = join(entry.parentPath, entry.name);
63
+ if (marker.test(await readFile(file, 'utf8')))
64
+ found.push(relative(process.cwd(), file));
65
+ }
66
+ }
67
+ return found;
68
+ }
69
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: typeof import("../../lib/base.js").LoopressCommand;
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { resourceRollbackCommand } from '../../lib/rollback-command.js';
2
+ export default resourceRollbackCommand('block', {
3
+ description: 'Restore dynamic blocks to a snapshot taken automatically before an earlier `lps block push`',
4
+ pathNoun: 'blocks directory',
5
+ });
@@ -4,6 +4,7 @@ import { writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { LoopressCommand } from '../../lib/base.js';
6
6
  import { isInteractive } from '../../lib/interactive.js';
7
+ import {} from '../../utils/composer.js';
7
8
  const WPACKAGIST_REPOSITORY = { type: 'composer', url: 'https://wpackagist.org' };
8
9
  const INSTALLERS_PACKAGE = 'composer/installers';
9
10
  const INSTALLERS_CONSTRAINT = '^2.0';
@@ -3,9 +3,10 @@ import { existsSync } from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { confirmUninstall } from '../../lib/interactive.js';
6
+ import { errorMessage } from '../../lib/narrow.js';
6
7
  import { PushCommand } from '../../lib/push-command.js';
7
8
  import { isLocalEnvironment, isTimeoutError } from '../../lib/wp-client.js';
8
- import { requireToInstall, toIntent } from '../../utils/composer.js';
9
+ import { parseComposerJson, requireToInstall, toIntent } from '../../utils/composer.js';
9
10
  import { parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
10
11
  export default class ComposerPush extends PushCommand {
11
12
  static description = 'Push composer.json to WordPress and run Composer to resolve and install dependencies';
@@ -25,7 +26,7 @@ export default class ComposerPush extends PushCommand {
25
26
  if (!existsSync(composerJsonPath)) {
26
27
  this.error(`No composer.json found at ${composerJsonPath}. Run \`lps composer init\` first.`);
27
28
  }
28
- const parsed = JSON.parse(await readFile(composerJsonPath, 'utf8'));
29
+ const parsed = parseComposerJson(await readFile(composerJsonPath, 'utf8'));
29
30
  const require = requireToInstall(parsed, isLocalEnvironment(this.siteConfig.name));
30
31
  const packageCount = Object.keys(require).length;
31
32
  const intent = toIntent(require);
@@ -57,7 +58,7 @@ export default class ComposerPush extends PushCommand {
57
58
  this.error('Plugins or themes are installed outside Loopress. Re-run with --force to take them over.');
58
59
  }
59
60
  if (isTimeoutError(error)) {
60
- this.error(`${error.message} The Composer run may still be in progress on the server.`);
61
+ this.error(`${errorMessage(error)} The Composer run may still be in progress on the server.`);
61
62
  }
62
63
  throw error;
63
64
  }
@@ -84,7 +85,7 @@ export default class ComposerPush extends PushCommand {
84
85
  }
85
86
  async wpackagistToRemove(require) {
86
87
  const { composerJson } = await this.wp.get('loopress/v1/composer/json');
87
- const current = JSON.parse(composerJson).require ?? {};
88
+ const current = parseComposerJson(composerJson).require ?? {};
88
89
  return Object.keys(current).filter((name) => name.startsWith('wpackagist-') && !Object.hasOwn(require, name));
89
90
  }
90
91
  }
@@ -22,7 +22,10 @@ export default class Copy extends LoopressCommand {
22
22
  };
23
23
  run(): Promise<ContentCopyResult>;
24
24
  protected sourceWp(from: string): WpClient;
25
+ private alignPlugins;
26
+ private alignTheme;
25
27
  private confirmReplace;
26
28
  private copy;
29
+ private readAlignment;
27
30
  }
28
31
  export {};