@loopress/cli 0.27.0 → 0.29.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 (124) hide show
  1. package/README.md +448 -189
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/acf/pull.d.ts +9 -1
  4. package/dist/commands/acf/pull.js +2 -0
  5. package/dist/commands/acf/push.d.ts +9 -1
  6. package/dist/commands/acf/push.js +3 -1
  7. package/dist/commands/api/publish.js +1 -1
  8. package/dist/commands/app/pull.js +17 -4
  9. package/dist/commands/app/push.d.ts +3 -0
  10. package/dist/commands/app/push.js +30 -6
  11. package/dist/commands/composer/push.js +3 -21
  12. package/dist/commands/diff.js +1 -1
  13. package/dist/commands/form/pull.d.ts +9 -1
  14. package/dist/commands/form/pull.js +3 -1
  15. package/dist/commands/form/push.d.ts +7 -1
  16. package/dist/commands/form/push.js +3 -1
  17. package/dist/commands/init.js +6 -36
  18. package/dist/commands/menu/pull.d.ts +7 -1
  19. package/dist/commands/menu/pull.js +2 -0
  20. package/dist/commands/menu/push.d.ts +7 -1
  21. package/dist/commands/menu/push.js +3 -1
  22. package/dist/commands/option/pull.js +4 -4
  23. package/dist/commands/option/push.js +2 -0
  24. package/dist/commands/page/push.js +3 -0
  25. package/dist/commands/plugin/add.js +6 -3
  26. package/dist/commands/plugin/audit.js +4 -5
  27. package/dist/commands/plugin/pull.d.ts +7 -1
  28. package/dist/commands/plugin/pull.js +68 -14
  29. package/dist/commands/plugin/push.d.ts +5 -1
  30. package/dist/commands/plugin/push.js +92 -38
  31. package/dist/commands/plugin/status.d.ts +1 -0
  32. package/dist/commands/plugin/status.js +21 -2
  33. package/dist/commands/project/config.js +12 -8
  34. package/dist/commands/promote.d.ts +2 -0
  35. package/dist/commands/promote.js +69 -8
  36. package/dist/commands/pull.js +2 -2
  37. package/dist/commands/push.d.ts +1 -0
  38. package/dist/commands/push.js +40 -5
  39. package/dist/commands/seo/pull.d.ts +8 -1
  40. package/dist/commands/seo/pull.js +2 -0
  41. package/dist/commands/seo/push.d.ts +7 -1
  42. package/dist/commands/seo/push.js +3 -1
  43. package/dist/commands/snippet/publish.js +1 -1
  44. package/dist/commands/snippet/pull.js +1 -1
  45. package/dist/commands/snippet/push.js +1 -1
  46. package/dist/commands/status.d.ts +1 -0
  47. package/dist/commands/status.js +4 -4
  48. package/dist/commands/theme/pull.d.ts +6 -8
  49. package/dist/commands/theme/pull.js +30 -26
  50. package/dist/commands/theme/push.d.ts +8 -9
  51. package/dist/commands/theme/push.js +54 -111
  52. package/dist/commands/theme/style/diff.d.ts +2 -0
  53. package/dist/commands/{theme-styles → theme/style}/diff.js +2 -1
  54. package/dist/commands/{theme-styles → theme/style}/pull.d.ts +8 -2
  55. package/dist/commands/{theme-styles → theme/style}/pull.js +6 -4
  56. package/dist/commands/{theme-styles → theme/style}/push.d.ts +9 -2
  57. package/dist/commands/{theme-styles → theme/style}/push.js +9 -6
  58. package/dist/commands/theme/style/rollback.d.ts +2 -0
  59. package/dist/commands/theme/style/rollback.js +6 -0
  60. package/dist/commands/theme/template/diff.d.ts +10 -0
  61. package/dist/commands/theme/template/diff.js +13 -0
  62. package/dist/commands/theme/template/list.d.ts +13 -0
  63. package/dist/commands/theme/template/list.js +23 -0
  64. package/dist/commands/theme/template/push.d.ts +20 -0
  65. package/dist/commands/theme/template/push.js +44 -0
  66. package/dist/commands/theme/version/pull.d.ts +23 -0
  67. package/dist/commands/theme/version/pull.js +43 -0
  68. package/dist/commands/theme/version/push.d.ts +22 -0
  69. package/dist/commands/theme/version/push.js +123 -0
  70. package/dist/hooks/finally.d.ts +2 -1
  71. package/dist/hooks/finally.js +27 -4
  72. package/dist/lib/api-client.js +3 -1
  73. package/dist/lib/app-build.d.ts +5 -0
  74. package/dist/lib/app-build.js +69 -0
  75. package/dist/lib/app-manifest.d.ts +4 -0
  76. package/dist/lib/app-manifest.js +10 -2
  77. package/dist/lib/base.d.ts +1 -0
  78. package/dist/lib/base.js +14 -2
  79. package/dist/lib/bootstrap-full-install.js +45 -14
  80. package/dist/lib/dev-watch.d.ts +1 -1
  81. package/dist/lib/dev-watch.js +3 -1
  82. package/dist/lib/diff-command.d.ts +1 -0
  83. package/dist/lib/diff-command.js +6 -4
  84. package/dist/lib/diff-state.js +1 -0
  85. package/dist/lib/github-release.js +0 -1
  86. package/dist/lib/local-callback-server.d.ts +12 -6
  87. package/dist/lib/local-callback-server.js +21 -22
  88. package/dist/lib/php-files-command.js +3 -1
  89. package/dist/lib/push-command.d.ts +5 -0
  90. package/dist/lib/push-command.js +30 -8
  91. package/dist/lib/resource-state.js +47 -4
  92. package/dist/lib/rollback-command.d.ts +1 -0
  93. package/dist/lib/rollback-command.js +8 -6
  94. package/dist/lib/rotate-app-password.js +2 -2
  95. package/dist/lib/snapshot-store.js +2 -2
  96. package/dist/lib/validate-local.js +6 -0
  97. package/dist/lib/wp-authorize-flow.d.ts +10 -17
  98. package/dist/lib/wp-authorize-flow.js +24 -28
  99. package/dist/lib/wp-client.d.ts +4 -1
  100. package/dist/lib/wp-client.js +25 -1
  101. package/dist/lib/wp-site-diagnostic.js +37 -0
  102. package/dist/types/plugin.d.ts +8 -1
  103. package/dist/types/plugin.js +4 -1
  104. package/dist/types/project-config.generated.d.ts +19 -2
  105. package/dist/utils/composer.d.ts +16 -0
  106. package/dist/utils/composer.js +144 -0
  107. package/dist/utils/option-format.d.ts +3 -1
  108. package/dist/utils/option-format.js +14 -3
  109. package/dist/utils/page-format.d.ts +12 -0
  110. package/dist/utils/page-format.js +40 -28
  111. package/dist/utils/plugin-sync.js +1 -1
  112. package/dist/utils/plugins.d.ts +9 -4
  113. package/dist/utils/plugins.js +16 -7
  114. package/dist/utils/resource-dirs.d.ts +2 -0
  115. package/dist/utils/resource-dirs.js +6 -2
  116. package/dist/utils/seo-format.d.ts +2 -2
  117. package/dist/utils/template-format.d.ts +31 -0
  118. package/dist/utils/template-format.js +43 -0
  119. package/oclif.manifest.json +841 -63
  120. package/package.json +24 -17
  121. package/schemas/project-config.schema.json +25 -4
  122. package/dist/commands/theme-styles/diff.d.ts +0 -2
  123. package/dist/commands/theme-styles/rollback.d.ts +0 -2
  124. package/dist/commands/theme-styles/rollback.js +0 -5
@@ -1,123 +1,66 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { confirmUninstall } from '../../lib/interactive.js';
5
- import { PushCommand } from '../../lib/push-command.js';
6
- import { isNotFoundError } from '../../lib/wp-client.js';
7
- import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS } from '../../utils/plugin-sync.js';
8
- import { lockedWpackagistSlugs } from '../../utils/plugins.js';
9
- import { diffThemes, parseInstalledThemes } from '../../utils/themes.js';
10
- const IN_SYNC = { installed: [], pinned: [], removed: [], status: 'in-sync' };
11
- export default class Push extends PushCommand {
12
- static description = 'Install themes on WordPress to match loopress.json (versions only, never switches the active theme)';
4
+ import { LoopressCommand } from '../../lib/base.js';
5
+ import { guardProductionPush } from '../../lib/guard-production-push.js';
6
+ import { stdoutToStderr } from '../../lib/json-delegation.js';
7
+ import { pluralize } from '../../utils/pluralize.js';
8
+ // The parent theme must be installed before its child theme is written, and Global Styles
9
+ // belong to whichever theme is active, so they go last.
10
+ const PUSH_TARGETS = [
11
+ { commandId: 'theme:version:push', label: 'theme versions' },
12
+ { commandId: 'theme:template:push', label: 'templates and parts' },
13
+ { commandId: 'theme:style:push', label: 'Global Styles' },
14
+ ];
15
+ export default class Push extends LoopressCommand {
16
+ static description = 'Push everything theme related to WordPress: theme versions, templates and parts, then Global Styles';
13
17
  static enableJsonFlag = true;
14
- static examples = ['$ lps theme push', '$ lps theme push --force'];
18
+ static examples = ['$ lps theme push', '$ lps theme push --env staging', '$ lps theme push --dry-run'];
15
19
  static flags = {
16
- ...PushCommand.dryRunFlag,
17
- ...PushCommand.yesFlag,
18
- force: Flags.boolean({ default: false, description: 'Allow downgrades and take over themes installed outside Loopress' }),
20
+ ...LoopressCommand.dryRunFlag,
21
+ ...LoopressCommand.yesFlag,
22
+ force: Flags.boolean({ default: false, description: 'Passed to `lps theme version push`' }),
19
23
  };
20
24
  async run() {
21
25
  const { flags } = await this.parse(Push);
22
- const { force } = flags;
23
- if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
24
- this.warn('This project has a composer.json, which is authoritative. Run `lps composer push` instead.');
25
- return { ...IN_SYNC, status: 'composer-managed' };
26
- }
27
- const manifest = this.localConfig.themes ?? {};
28
- if (Object.keys(manifest).length === 0) {
29
- this.error('No themes found in loopress.json. Run `lps theme pull` first.');
30
- }
31
- this.log(`Pushing themes to ${this.siteConfig.url}`);
32
- const raw = await this.wp.get('wp/v2/themes');
33
- const installed = parseInstalledThemes(raw);
34
- const managed = lockedWpackagistSlugs(await this.fetchInstanceLock(), 'theme');
35
- const diff = diffThemes(manifest, installed, managed);
36
- this.guardForce(diff, force);
37
- // Uninstalling the theme WordPress is currently rendering leaves the site with no active
38
- // theme, which is a hard fatal. --force does not override this: switch the active theme
39
- // on the site first, then the removal is safe. A --force takeover of a collision hits the
40
- // same fatal: the server moves the existing folder out of the way before reinstalling it,
41
- // so the active theme is briefly missing either way, an active collision is just as
42
- // dangerous as an active removal.
43
- const activeRemoval = [...diff.toRemove, ...diff.collisions.map((c) => c.slug)].find((slug) => installed.find((t) => t.slug === slug)?.active);
44
- if (activeRemoval) {
45
- this.error(`Refusing to uninstall "${activeRemoval}": it is the site's active theme. ` +
46
- 'Switch the site to another theme, then re-run to remove it.');
47
- }
48
- // A "latest" pin never shows as drift, but its newest upstream release may have moved, so
49
- // a push must still run `composer update` to pick it up.
50
- const hasLatestPin = Object.values(manifest).includes('latest');
51
- if (!hasLatestPin && isNoop(diff)) {
52
- this.log('Everything is already in sync.');
53
- return { ...IN_SYNC };
54
- }
55
- if (isNoop(diff))
56
- this.log('Refreshing themes pinned to "latest" to their newest releases.');
57
- for (const a of diff.toInstall)
58
- this.log(` + ${a.slug} ${a.version}`);
59
- for (const p of diff.toPin)
60
- this.log(` ~ ${p.slug} ${p.from} to ${p.to}`);
61
- for (const s of diff.toRemove)
62
- this.log(` - ${s}`);
63
- for (const c of diff.collisions)
64
- this.log(` ! ${c.slug} (take over)`);
65
- if (this.dryRun)
66
- return result(diff, 'dry-run', diff.toRemove, true);
67
- if (!(await confirmUninstall(diff.toRemove, this.yes)))
68
- this.error('Aborted.');
69
- const response = await this.sync(manifest, force);
70
- if (response.output.trim())
71
- this.log(response.output.trim());
72
- this.log('Themes synced.');
73
- await this.recordSuccess();
74
- return result(diff, 'success', response.removed ?? diff.toRemove, force);
75
- }
76
- async fetchInstanceLock() {
77
- try {
78
- const { composerLock } = await this.wp.get('loopress/v1/composer/lock');
79
- return composerLock;
80
- }
81
- catch (error) {
82
- if (isNotFoundError(error))
83
- return null;
84
- throw error;
85
- }
86
- }
87
- guardForce(diff, force) {
88
- if (force)
89
- return;
90
- if (diff.collisions.length > 0) {
91
- const list = diff.collisions.map((c) => `${c.slug} (${c.installedVersion})`).join(', ');
92
- this.error(`${list} installed outside Loopress. Re-run with --force to take them over, or remove them from loopress.json.`);
93
- }
94
- const downgrades = diff.toPin.filter((p) => isDowngrade(p.from, p.to));
95
- if (downgrades.length > 0) {
96
- const list = downgrades.map((p) => `${p.slug} ${p.from} to ${p.to}`).join(', ');
97
- this.error(`Refusing to downgrade: ${list}. Re-run with --force.`);
98
- }
99
- }
100
- async sync(manifest, force) {
101
- try {
102
- return await this.wp.post('loopress/v1/composer/sync', { force, intent: { themes: manifest }, lock: null }, { timeoutMs: SYNC_TIMEOUT_MS });
103
- }
104
- catch (error) {
105
- if (parseCollisions(error)) {
106
- this.error('The site rejected the push: themes installed outside Loopress. Re-run with --force.');
26
+ // Guarded once here, like `lps push`. Steps get --production-confirmed rather than --yes,
27
+ // so the versions step still asks before uninstalling themes unless the user passed --yes.
28
+ await guardProductionPush({
29
+ dryRun: this.dryRun,
30
+ error: (message) => this.error(message),
31
+ siteConfig: this.siteConfig,
32
+ yes: this.yes,
33
+ });
34
+ // Nothing to install and no composer.json: the versions step would only fail on an empty
35
+ // manifest, which must not fail a templates or styles only project.
36
+ const hasVersions = Object.keys(this.localConfig.themes ?? {}).length > 0 || existsSync(join(process.cwd(), this.rootDir, 'composer.json'));
37
+ const targets = hasVersions ? PUSH_TARGETS : PUSH_TARGETS.filter((target) => target.commandId !== 'theme:version:push');
38
+ const results = [];
39
+ for (const target of targets) {
40
+ const argv = ['--env', this.siteConfig.name, '--production-confirmed'];
41
+ if (this.yes)
42
+ argv.push('--yes');
43
+ if (this.dryRun)
44
+ argv.push('--dry-run');
45
+ if (flags.force && target.commandId === 'theme:version:push')
46
+ argv.push('--force');
47
+ this.log(`\n→ Pushing ${target.label}...`);
48
+ try {
49
+ await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand(target.commandId, argv));
50
+ this.log(`✓ ${target.label} pushed`);
51
+ results.push({ label: target.label, status: 'pushed' });
52
+ }
53
+ catch (error) {
54
+ const { message } = error;
55
+ this.log(`✗ ${target.label} failed: ${message}`);
56
+ results.push({ error: message, label: target.label, status: 'failed' });
107
57
  }
108
- throw error;
109
58
  }
59
+ const failures = results.filter((result) => result.status === 'failed');
60
+ if (failures.length > 0) {
61
+ const reasons = failures.map((result) => `${result.label}: ${result.error}`);
62
+ this.error(`${pluralize(failures.length, 'theme resource')} failed to push. ${reasons.join('; ')}`);
63
+ }
64
+ return { results };
110
65
  }
111
66
  }
112
- function isNoop(diff) {
113
- return (diff.toInstall.length === 0 && diff.toPin.length === 0 && diff.toRemove.length === 0 && diff.collisions.length === 0);
114
- }
115
- function result(diff, status, removed, includeCollisions) {
116
- const collisionSlugs = includeCollisions ? diff.collisions.map((c) => c.slug) : [];
117
- return {
118
- installed: [...diff.toInstall.map((a) => a.slug), ...collisionSlugs],
119
- pinned: diff.toPin.map((p) => p.slug),
120
- removed,
121
- status,
122
- };
123
- }
@@ -0,0 +1,2 @@
1
+ declare const _default: typeof import("../../../lib/diff-command.js").DiffCommand;
2
+ export default _default;
@@ -1,5 +1,6 @@
1
- import { resourceDiffCommand } from '../../lib/diff-command.js';
1
+ import { resourceDiffCommand } from '../../../lib/diff-command.js';
2
2
  export default resourceDiffCommand('theme-styles', {
3
3
  description: "Show what differs in the active block theme's Global Styles (Site Editor > Styles) between your local file and a WordPress environment, or between two environments",
4
+ command: 'theme style',
4
5
  pathNoun: 'theme styles directory',
5
6
  });
@@ -1,12 +1,18 @@
1
- import { LoopressCommand } from '../../lib/base.js';
1
+ import { LoopressCommand } from '../../../lib/base.js';
2
+ type ThemeStylePullResult = {
3
+ file: string;
4
+ status: 'dry-run' | 'success';
5
+ };
2
6
  export default class Pull extends LoopressCommand {
3
7
  static args: {
4
8
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
9
  };
6
10
  static description: string;
11
+ static enableJsonFlag: boolean;
7
12
  static examples: string[];
8
13
  static flags: {
9
14
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
15
  };
11
- run(): Promise<void>;
16
+ run(): Promise<ThemeStylePullResult>;
12
17
  }
18
+ export {};
@@ -1,14 +1,15 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { LoopressCommand } from '../../lib/base.js';
5
- import { canonicalGlobalStyles, getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName, } from '../../utils/theme-styles-format.js';
4
+ import { LoopressCommand } from '../../../lib/base.js';
5
+ import { canonicalGlobalStyles, getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName, } from '../../../utils/theme-styles-format.js';
6
6
  export default class Pull extends LoopressCommand {
7
7
  static args = {
8
8
  path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
9
9
  };
10
10
  static description = "Pull the active block theme's Global Styles customizations (Site Editor > Styles) from WordPress into a local file";
11
- static examples = ['$ lps theme-styles pull'];
11
+ static enableJsonFlag = true;
12
+ static examples = ['$ lps theme style pull'];
12
13
  static flags = {
13
14
  ...LoopressCommand.dryRunFlag,
14
15
  };
@@ -22,10 +23,11 @@ export default class Pull extends LoopressCommand {
22
23
  const file = join(dir, themeStylesFileName(stylesheet));
23
24
  if (this.dryRun) {
24
25
  this.log(`[dry-run] Would pull Global Styles for "${stylesheet}" to ${file}`);
25
- return;
26
+ return { file, status: 'dry-run' };
26
27
  }
27
28
  await mkdir(dir, { recursive: true });
28
29
  await writeFile(file, JSON.stringify(canonicalGlobalStyles(item), null, 2) + '\n');
29
30
  this.log(`Pulled Global Styles for "${stylesheet}" to ${file}`);
31
+ return { file, status: 'success' };
30
32
  }
31
33
  }
@@ -1,12 +1,19 @@
1
- import { PushCommand } from '../../lib/push-command.js';
1
+ import { PushCommand } from '../../../lib/push-command.js';
2
+ type ThemeStylePushResult = {
3
+ file: string;
4
+ status: 'dry-run' | 'success';
5
+ };
2
6
  export default class Push extends PushCommand {
3
7
  static args: {
4
8
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
9
  };
6
10
  static description: string;
11
+ static enableJsonFlag: boolean;
7
12
  static examples: string[];
8
13
  static flags: {
14
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
15
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
16
  };
11
- run(): Promise<void>;
17
+ run(): Promise<ThemeStylePushResult>;
12
18
  }
19
+ export {};
@@ -1,17 +1,19 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { PushCommand } from '../../lib/push-command.js';
5
- import { getResourceStateProvider } from '../../lib/resource-state.js';
6
- import { getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName } from '../../utils/theme-styles-format.js';
4
+ import { PushCommand } from '../../../lib/push-command.js';
5
+ import { getResourceStateProvider } from '../../../lib/resource-state.js';
6
+ import { getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName } from '../../../utils/theme-styles-format.js';
7
7
  export default class Push extends PushCommand {
8
8
  static args = {
9
9
  path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
10
10
  };
11
11
  static description = "Push the local Global Styles file to the active block theme's Site Editor > Styles on WordPress";
12
- static examples = ['$ lps theme-styles push'];
12
+ static enableJsonFlag = true;
13
+ static examples = ['$ lps theme style push'];
13
14
  static flags = {
14
15
  ...PushCommand.dryRunFlag,
16
+ ...PushCommand.yesFlag,
15
17
  };
16
18
  async run() {
17
19
  const { args } = await this.parse(Push);
@@ -26,13 +28,13 @@ export default class Push extends PushCommand {
26
28
  }
27
29
  catch (error) {
28
30
  if (error.code === 'ENOENT') {
29
- this.error(`No local Global Styles file found for the active theme ("${file}"). Run \`lps theme-styles pull\` first.`);
31
+ this.error(`No local Global Styles file found for the active theme ("${file}"). Run \`lps theme style pull\` first.`);
30
32
  }
31
33
  throw error;
32
34
  }
33
35
  if (this.dryRun) {
34
36
  this.log(`[dry-run] Would push: ${file}`);
35
- return;
37
+ return { file, status: 'dry-run' };
36
38
  }
37
39
  const provider = getResourceStateProvider('theme-styles');
38
40
  const beforeState = await this.captureBeforePushState(provider, dir);
@@ -41,5 +43,6 @@ export default class Push extends PushCommand {
41
43
  this.log(`Pushed: ${file}`);
42
44
  await this.writeAfterPushSnapshot(provider, dir, beforeState);
43
45
  await this.recordSuccess();
46
+ return { file, status: 'success' };
44
47
  }
45
48
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: typeof import("../../../lib/base.js").LoopressCommand;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { resourceRollbackCommand } from '../../../lib/rollback-command.js';
2
+ export default resourceRollbackCommand('theme-styles', {
3
+ description: "Restore the active theme's Global Styles to a snapshot taken automatically before an earlier `lps theme style push`",
4
+ command: 'theme style',
5
+ pathNoun: 'theme styles directory',
6
+ });
@@ -0,0 +1,10 @@
1
+ import { DiffCommand, type DiffJson } from '../../../lib/diff-command.js';
2
+ export default class Diff extends DiffCommand {
3
+ static description: string;
4
+ static enableJsonFlag: boolean;
5
+ static examples: string[];
6
+ static flags: {
7
+ against: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<DiffJson>;
10
+ }
@@ -0,0 +1,13 @@
1
+ import { DiffCommand } from '../../../lib/diff-command.js';
2
+ import { getResourceStateProvider } from '../../../lib/resource-state.js';
3
+ export default class Diff extends DiffCommand {
4
+ static description = 'Show what differs in block templates and parts between your local files and a WordPress environment, or between two environments. Templates and parts edited in the Site Editor show up too.';
5
+ static enableJsonFlag = true;
6
+ static examples = ['$ lps theme template diff', '$ lps theme template diff --env staging', '$ lps theme template diff --env staging --against production'];
7
+ static flags = { ...DiffCommand.againstFlag };
8
+ async run() {
9
+ const { flags } = await this.parse(Diff);
10
+ const sides = this.resolveSides(flags.against);
11
+ return this.report(['template', 'part'].map((resource) => this.providerTarget(getResourceStateProvider(resource), sides)), sides);
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { LoopressCommand } from '../../../lib/base.js';
2
+ import { type ChildTheme } from '../../../utils/template-format.js';
3
+ type ListResult = Omit<ChildTheme, 'parts' | 'templates'> & {
4
+ parts: string[];
5
+ templates: string[];
6
+ };
7
+ export default class List extends LoopressCommand {
8
+ static description: string;
9
+ static enableJsonFlag: boolean;
10
+ static examples: string[];
11
+ run(): Promise<ListResult>;
12
+ }
13
+ export {};
@@ -0,0 +1,23 @@
1
+ import { LoopressCommand } from '../../../lib/base.js';
2
+ import { pluralize } from '../../../utils/pluralize.js';
3
+ import { CHILD_THEME_ENDPOINT } from '../../../utils/template-format.js';
4
+ export default class List extends LoopressCommand {
5
+ static description = 'Show the Loopress child theme of the active block theme: its templates, parts, and those edited in the Site Editor';
6
+ static enableJsonFlag = true;
7
+ static examples = ['$ lps theme template list'];
8
+ async run() {
9
+ const child = await this.wp.get(CHILD_THEME_ENDPOINT);
10
+ // The markup is only needed by `template diff`, it would drown the JSON output here.
11
+ const result = { ...child, parts: child.parts.map((part) => part.slug), templates: child.templates.map((template) => template.slug) };
12
+ if (!child.exists) {
13
+ this.log(`No Loopress child theme yet (${child.stylesheet}), run lps theme template push.`);
14
+ return result;
15
+ }
16
+ this.log(`${child.stylesheet} (child of ${child.parent}, ${child.active ? 'active' : 'not active'})`);
17
+ this.log(` ${pluralize(result.templates.length, 'template')}: ${result.templates.join(', ') || 'none'}`);
18
+ this.log(` ${pluralize(result.parts.length, 'part')}: ${result.parts.join(', ') || 'none'}`);
19
+ if (child.customized.length > 0)
20
+ this.log(` Edited in the Site Editor: ${child.customized.join(', ')}`);
21
+ return result;
22
+ }
23
+ }
@@ -0,0 +1,20 @@
1
+ import { PushCommand } from '../../../lib/push-command.js';
2
+ type PushResult = {
3
+ active?: boolean;
4
+ customized?: string[];
5
+ parts: string[];
6
+ status: 'dry-run' | 'success';
7
+ stylesheet?: string;
8
+ templates: string[];
9
+ };
10
+ export default class Push extends PushCommand {
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,44 @@
1
+ import { PushCommand } from '../../../lib/push-command.js';
2
+ import { getResourceStateProvider } from '../../../lib/resource-state.js';
3
+ import { formatPageProblems } from '../../../utils/page-format.js';
4
+ import { pluralize } from '../../../utils/pluralize.js';
5
+ import { resolveResourceDir } from '../../../utils/resource-dirs.js';
6
+ import { CHILD_THEME_ENDPOINT, readLocalTemplates } from '../../../utils/template-format.js';
7
+ export default class Push extends PushCommand {
8
+ static description = 'Push block templates (templates/<slug>.html) and template parts (parts/<slug>.html) as the files of a child theme of the active block theme, <parent>-loopress. The child mirrors the project: files removed locally are removed from it. It is never activated for you.';
9
+ static enableJsonFlag = true;
10
+ static examples = ['$ lps theme template push', '$ lps theme template push --dry-run'];
11
+ static flags = {
12
+ ...PushCommand.dryRunFlag,
13
+ ...PushCommand.yesFlag,
14
+ };
15
+ async run() {
16
+ await this.parse(Push);
17
+ const templatesPath = resolveResourceDir('template', this.localConfig);
18
+ const partsPath = resolveResourceDir('part', this.localConfig);
19
+ this.log(`Pushing templates and parts to ${this.siteConfig.url}`);
20
+ this.log(`Templates path: ${templatesPath}, parts path: ${partsPath}`);
21
+ const { parts, problems, templates } = await readLocalTemplates(templatesPath, partsPath);
22
+ if (problems.length > 0) {
23
+ this.error(`Nothing was pushed, fix these files first:\n ${formatPageProblems(problems)}`);
24
+ }
25
+ const names = { parts: parts.map((part) => part.slug), templates: templates.map((template) => template.slug) };
26
+ this.log(`Found ${pluralize(templates.length, 'template')} and ${pluralize(parts.length, 'part')}`);
27
+ if (this.dryRun) {
28
+ await this.previewRemote(getResourceStateProvider('template'), templatesPath);
29
+ this.log('[dry-run] Would rewrite the child theme with them.');
30
+ return { ...names, status: 'dry-run' };
31
+ }
32
+ const child = await this.wp.put(CHILD_THEME_ENDPOINT, { parts, templates });
33
+ await this.recordSuccess();
34
+ this.log(`Child theme ${child.stylesheet} written.`);
35
+ if (!child.active) {
36
+ this.warn(`${child.stylesheet} is not the active theme, so nothing changed on the site yet. Activate it in Appearance > Themes.`);
37
+ }
38
+ if (child.customized.length > 0) {
39
+ this.warn(`Edited in the Site Editor, their database copy still wins over the pushed file: ${child.customized.join(', ')}. ` +
40
+ 'Clear their customizations in Appearance > Editor to use the pushed version.');
41
+ }
42
+ return { ...names, active: child.active, customized: child.customized, status: 'success', stylesheet: child.stylesheet };
43
+ }
44
+ }
@@ -0,0 +1,23 @@
1
+ import { LoopressCommand } from '../../../lib/base.js';
2
+ import { type ComposerPullResult } from '../../../utils/composer.js';
3
+ type PullResult = {
4
+ added: string[];
5
+ merged: Record<string, string>;
6
+ skipped?: ComposerPullResult['skipped'];
7
+ status: 'dry-run' | 'success';
8
+ updated: Array<{
9
+ from: string;
10
+ slug: string;
11
+ to: string;
12
+ }>;
13
+ };
14
+ export default class Pull extends LoopressCommand {
15
+ static description: string;
16
+ static enableJsonFlag: boolean;
17
+ static examples: string[];
18
+ static flags: {
19
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ };
21
+ run(): Promise<PullResult>;
22
+ }
23
+ export {};
@@ -0,0 +1,43 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { LoopressCommand } from '../../../lib/base.js';
4
+ import { pullIntoComposerJson } from '../../../utils/composer.js';
5
+ import { writeLocalConfig } from '../../../utils/loopress-config.js';
6
+ import { mergePluginManifest } from '../../../utils/plugins.js';
7
+ import { parseInstalledThemes } from '../../../utils/themes.js';
8
+ export default class Pull extends LoopressCommand {
9
+ static description = 'Pull installed themes from WordPress into loopress.json, pinned to their live versions';
10
+ static enableJsonFlag = true;
11
+ static examples = ['$ lps theme version pull'];
12
+ static flags = {
13
+ ...LoopressCommand.dryRunFlag,
14
+ };
15
+ async run() {
16
+ const { url } = this.siteConfig;
17
+ this.log(`Pulling themes from ${url}`);
18
+ const raw = await this.wp.get('wp/v2/themes');
19
+ const installed = parseInstalledThemes(raw);
20
+ const incoming = Object.fromEntries(installed.map((t) => [t.slug, t.version]));
21
+ // A composer.json is authoritative over loopress.json: pin the live versions there instead.
22
+ const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
23
+ if (existsSync(composerJsonPath)) {
24
+ const result = await pullIntoComposerJson(composerJsonPath, 'theme', incoming, {
25
+ dryRun: this.dryRun,
26
+ log: this.log.bind(this),
27
+ });
28
+ return { ...result, status: this.dryRun ? 'dry-run' : 'success' };
29
+ }
30
+ const { added, merged, updated } = mergePluginManifest(this.localConfig.themes ?? {}, incoming);
31
+ if (this.dryRun) {
32
+ this.log(`[dry-run] Would write ${Object.keys(merged).length} themes to loopress.json`);
33
+ return { added, merged, status: 'dry-run', updated };
34
+ }
35
+ await writeLocalConfig({ ...this.localConfig, themes: merged });
36
+ this.log(`Wrote ${Object.keys(merged).length} themes to loopress.json`);
37
+ if (added.length > 0)
38
+ this.log(` + Added: ${added.join(', ')}`);
39
+ for (const u of updated)
40
+ this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
41
+ return { added, merged, status: 'success', updated };
42
+ }
43
+ }
@@ -0,0 +1,22 @@
1
+ import { PushCommand } from '../../../lib/push-command.js';
2
+ type PushResult = {
3
+ installed: string[];
4
+ pinned: string[];
5
+ removed: string[];
6
+ status: 'composer-managed' | 'dry-run' | 'in-sync' | 'success';
7
+ };
8
+ export default class Push extends PushCommand {
9
+ static description: string;
10
+ static enableJsonFlag: boolean;
11
+ static examples: string[];
12
+ static flags: {
13
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<PushResult>;
18
+ private fetchInstanceLock;
19
+ private guardForce;
20
+ private sync;
21
+ }
22
+ export {};