@loopress/cli 0.31.0 → 0.32.1

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 (72) hide show
  1. package/README.md +440 -221
  2. package/dist/commands/acf/diff.d.ts +2 -2
  3. package/dist/commands/acf/diff.js +3 -1
  4. package/dist/commands/acf/list.d.ts +1 -0
  5. package/dist/commands/acf/list.js +2 -1
  6. package/dist/commands/acf/pull.d.ts +1 -0
  7. package/dist/commands/acf/pull.js +2 -1
  8. package/dist/commands/acf/push.d.ts +7 -2
  9. package/dist/commands/acf/push.js +12 -4
  10. package/dist/commands/acf/rollback.d.ts +2 -2
  11. package/dist/commands/acf/rollback.js +3 -1
  12. package/dist/commands/composer/pull.js +1 -1
  13. package/dist/commands/diff.d.ts +1 -0
  14. package/dist/commands/diff.js +13 -2
  15. package/dist/commands/doctor.js +1 -1
  16. package/dist/commands/form/list.js +1 -1
  17. package/dist/commands/form/pull.js +1 -1
  18. package/dist/commands/form/push.d.ts +2 -1
  19. package/dist/commands/form/push.js +2 -2
  20. package/dist/commands/init.js +0 -23
  21. package/dist/commands/menu/push.d.ts +2 -1
  22. package/dist/commands/menu/push.js +1 -1
  23. package/dist/commands/option/push.d.ts +2 -1
  24. package/dist/commands/option/push.js +1 -1
  25. package/dist/commands/page/push.d.ts +4 -1
  26. package/dist/commands/page/push.js +40 -8
  27. package/dist/commands/pattern/push.js +1 -1
  28. package/dist/commands/pattern/rm.js +1 -1
  29. package/dist/commands/plugin/audit.js +1 -1
  30. package/dist/commands/plugin/push.js +1 -1
  31. package/dist/commands/plugin/status.js +1 -1
  32. package/dist/commands/pull.d.ts +1 -1
  33. package/dist/commands/pull.js +22 -10
  34. package/dist/commands/push.d.ts +2 -0
  35. package/dist/commands/push.js +29 -9
  36. package/dist/commands/snippet/list.js +1 -1
  37. package/dist/commands/snippet/pull.js +1 -1
  38. package/dist/commands/snippet/push.d.ts +2 -1
  39. package/dist/commands/snippet/push.js +2 -2
  40. package/dist/commands/theme/pull.js +1 -1
  41. package/dist/commands/theme/push.d.ts +1 -0
  42. package/dist/commands/theme/push.js +3 -3
  43. package/dist/commands/theme/style/push.d.ts +2 -1
  44. package/dist/commands/theme/style/push.js +4 -5
  45. package/dist/commands/theme/template/list.js +1 -1
  46. package/dist/commands/theme/template/push.d.ts +11 -1
  47. package/dist/commands/theme/template/push.js +34 -5
  48. package/dist/commands/theme/version/push.js +1 -1
  49. package/dist/lib/declared-type-command.d.ts +2 -1
  50. package/dist/lib/declared-type-command.js +1 -1
  51. package/dist/lib/diff-command.d.ts +1 -0
  52. package/dist/lib/diff-command.js +10 -2
  53. package/dist/lib/php-files-command.d.ts +3 -1
  54. package/dist/lib/php-files-command.js +34 -19
  55. package/dist/lib/plugin-detection.d.ts +1 -0
  56. package/dist/lib/plugin-detection.js +4 -0
  57. package/dist/lib/push-command.d.ts +17 -0
  58. package/dist/lib/push-command.js +42 -3
  59. package/dist/lib/validate-local.js +12 -5
  60. package/dist/lib/wp-client.d.ts +1 -0
  61. package/dist/lib/wp-client.js +13 -0
  62. package/dist/utils/acf-format.d.ts +5 -0
  63. package/dist/utils/acf-format.js +29 -0
  64. package/dist/utils/php-top-level.d.ts +2 -0
  65. package/dist/utils/php-top-level.js +153 -0
  66. package/dist/utils/resource-dirs.d.ts +1 -0
  67. package/dist/utils/resource-dirs.js +7 -1
  68. package/dist/utils/template-format.d.ts +1 -0
  69. package/dist/utils/theme-styles-format.d.ts +2 -0
  70. package/dist/utils/theme-styles-format.js +32 -4
  71. package/oclif.manifest.json +548 -537
  72. package/package.json +2 -2
@@ -1,2 +1,2 @@
1
- declare const _default: typeof import("../../lib/diff-command.js").DiffCommand;
2
- export default _default;
1
+ declare const Command: typeof import("../../lib/diff-command.js").DiffCommand;
2
+ export default Command;
@@ -1,5 +1,7 @@
1
1
  import { resourceDiffCommand } from '../../lib/diff-command.js';
2
- export default resourceDiffCommand('acf', {
2
+ const Command = resourceDiffCommand('acf', {
3
3
  description: 'Show what differs in ACF field groups, post types, taxonomies, and options pages between your local files and a WordPress environment, or between two environments',
4
4
  pathNoun: 'ACF directory',
5
5
  });
6
+ Command.aliases = ['scf:diff'];
7
+ export default Command;
@@ -1,5 +1,6 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
2
  export default class List extends LoopressCommand {
3
+ static aliases: string[];
3
4
  static description: string;
4
5
  static enableJsonFlag: boolean;
5
6
  static examples: string[];
@@ -2,7 +2,8 @@ import { Flags } from '@oclif/core';
2
2
  import { LoopressCommand } from '../../lib/base.js';
3
3
  import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
4
4
  export default class List extends LoopressCommand {
5
- static description = 'List ACF field groups, post types, taxonomies, and options pages from WordPress';
5
+ static aliases = ['scf:list'];
6
+ static description = 'List ACF field groups, post types, taxonomies, and options pages currently on WordPress';
6
7
  static enableJsonFlag = true;
7
8
  static examples = ['$ lps acf list', '$ lps acf list --type field-groups'];
8
9
  static flags = {
@@ -6,6 +6,7 @@ type AcfPullResult = {
6
6
  types: AcfObjectType[];
7
7
  };
8
8
  export default class Pull extends LoopressCommand {
9
+ static aliases: string[];
9
10
  static args: {
10
11
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
11
12
  };
@@ -5,10 +5,11 @@ import { LoopressCommand } from '../../lib/base.js';
5
5
  import { basenameKey, findOrphanedFiles } from '../../lib/find-orphaned-files.js';
6
6
  import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
7
7
  export default class Pull extends LoopressCommand {
8
+ static aliases = ['scf:pull'];
8
9
  static args = {
9
10
  path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
10
11
  };
11
- static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress';
12
+ static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress into local files';
12
13
  static enableJsonFlag = true;
13
14
  static examples = ['$ lps acf pull', '$ lps acf pull --type field-groups'];
14
15
  static flags = {
@@ -1,11 +1,16 @@
1
- import { PushCommand } from '../../lib/push-command.js';
2
- import { type AcfObjectType } from '../../utils/acf-format.js';
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
+ import { acfFieldChanges, type AcfObjectType } from '../../utils/acf-format.js';
3
+ type AcfChange = PushChange & {
4
+ fields?: ReturnType<typeof acfFieldChanges>;
5
+ };
3
6
  type AcfPushResult = {
7
+ changes?: AcfChange[];
4
8
  path: string;
5
9
  status: 'dry-run' | 'success';
6
10
  types: AcfObjectType[];
7
11
  };
8
12
  export default class Push extends PushCommand {
13
+ static aliases: string[];
9
14
  static args: {
10
15
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
11
16
  };
@@ -4,13 +4,14 @@ import { loadFiles } from '../../lib/load-files.js';
4
4
  import { PushCommand } from '../../lib/push-command.js';
5
5
  import { getResourceStateProvider } from '../../lib/resource-state.js';
6
6
  import { isNotFoundError } from '../../lib/wp-client.js';
7
- import { ACF_OBJECT_TYPES, acfEndpoint, acfObjectEndpoint, getAcfKey } from '../../utils/acf-format.js';
7
+ import { ACF_OBJECT_TYPES, acfEndpoint, acfFieldChanges, acfObjectEndpoint, getAcfKey } from '../../utils/acf-format.js';
8
8
  import { pluralize } from '../../utils/pluralize.js';
9
9
  export default class Push extends PushCommand {
10
+ static aliases = ['scf:push'];
10
11
  static args = {
11
12
  path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
12
13
  };
13
- static description = 'Push ACF field groups, post types, taxonomies, and options pages to WordPress';
14
+ static description = 'Push local ACF field groups, post types, taxonomies, and options pages to WordPress';
14
15
  static enableJsonFlag = true;
15
16
  static examples = ['$ lps acf push', '$ lps acf push --type field-groups'];
16
17
  static flags = {
@@ -35,8 +36,15 @@ export default class Push extends PushCommand {
35
36
  if (this.failedCount > 0) {
36
37
  this.error(`${pluralize(this.failedCount, 'ACF object')} failed to push.`);
37
38
  }
38
- if (this.dryRun)
39
- return { path, status: 'dry-run', types };
39
+ if (this.dryRun) {
40
+ const states = this.previewStates;
41
+ const changes = this.previewChanges.map((change) => {
42
+ if (!states || change.status !== 'changed' || !change.id.startsWith('field-groups/'))
43
+ return change;
44
+ return { ...change, fields: acfFieldChanges(states.remote.get(change.id), states.local.get(change.id)) };
45
+ });
46
+ return { changes, path, status: 'dry-run', types };
47
+ }
40
48
  await this.recordSuccess();
41
49
  this.log('All ACF objects pushed.');
42
50
  return { path, status: 'success', types };
@@ -1,2 +1,2 @@
1
- declare const _default: typeof import("../../lib/base.js").LoopressCommand;
2
- export default _default;
1
+ declare const Command: typeof import("../../lib/base.js").LoopressCommand;
2
+ export default Command;
@@ -1,5 +1,7 @@
1
1
  import { resourceRollbackCommand } from '../../lib/rollback-command.js';
2
- export default resourceRollbackCommand('acf', {
2
+ const Command = resourceRollbackCommand('acf', {
3
3
  description: 'Restore ACF field groups, post types, taxonomies, and options pages to a snapshot taken automatically before an earlier `lps acf push`',
4
4
  pathNoun: 'ACF directory',
5
5
  });
6
+ Command.aliases = ['scf:rollback'];
7
+ export default Command;
@@ -10,7 +10,7 @@ function isMissingComposerLock(error) {
10
10
  return isApplicative404(error, 'composer.lock not found');
11
11
  }
12
12
  export default class ComposerPull extends LoopressCommand {
13
- static description = 'Pull composer.json and composer.lock from WordPress';
13
+ static description = 'Pull composer.json and composer.lock from WordPress into local files';
14
14
  static enableJsonFlag = true;
15
15
  static examples = ['$ lps composer pull', '$ lps composer pull --dry-run'];
16
16
  static flags = {
@@ -9,5 +9,6 @@ export default class Diff extends DiffCommand {
9
9
  against: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  };
11
11
  run(): Promise<DiffJson>;
12
+ private projectUses;
12
13
  private selectResources;
13
14
  }
@@ -1,13 +1,15 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { DiffCommand } from '../lib/diff-command.js';
3
3
  import { RESOURCE_STATE_PROVIDERS } from '../lib/resource-state.js';
4
+ import { projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
5
+ import { globalStylesFiles } from '../utils/theme-styles-format.js';
4
6
  // Everything `lps diff` can compare, in report order. Composer isn't a directory-backed
5
7
  // provider (see resource-state.ts) so it's named here explicitly.
6
8
  const RESOURCES = [...RESOURCE_STATE_PROVIDERS.map((provider) => provider.resource), 'composer'];
7
9
  export default class Diff extends DiffCommand {
8
10
  static description = [
9
11
  'Show what differs between your local tracked files and a WordPress environment, or between two environments.',
10
- 'Covers snippets, forms, ACF, API routes, Hooks, templates, template parts, pages, Options, Menus, Theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
12
+ 'Covers snippets, forms, ACF, API routes, hooks, templates, template parts, pages, menus, custom post types, taxonomies, post meta, roles, block patterns, options, theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
11
13
  'Exit code: 0 in sync, 1 on drift, 2 when a resource could not be compared, so it doubles as a CI drift gate.',
12
14
  ].join(' ');
13
15
  static enableJsonFlag = true;
@@ -27,12 +29,21 @@ export default class Diff extends DiffCommand {
27
29
  const { flags } = await this.parse(Diff);
28
30
  const sides = this.resolveSides(flags.against);
29
31
  const selected = this.selectResources(flags.only, flags.skip);
32
+ const providers = RESOURCE_STATE_PROVIDERS.filter((provider) => selected.has(provider.resource));
30
33
  const targets = [
31
- ...RESOURCE_STATE_PROVIDERS.filter((provider) => selected.has(provider.resource)).map((provider) => this.providerTarget(provider, sides)),
34
+ ...(await Promise.all(providers.map(async (provider) => ({ ...this.providerTarget(provider, sides), optional: !(await this.projectUses(provider)) })))),
32
35
  ...(selected.has('composer') ? [this.composerTarget(sides)] : []),
33
36
  ];
34
37
  return this.report(targets, sides);
35
38
  }
39
+ // Tells apart a resource the project doesn't use from one it does, for the ones a site can lack:
40
+ // a plugin-backed resource without its directory, Global Styles without a tracked file.
41
+ async projectUses(provider) {
42
+ if (provider.dirKind === 'themeStyles') {
43
+ return (await globalStylesFiles(resolveResourceDir('themeStyles', this.localConfig))).length > 0;
44
+ }
45
+ return projectUses(provider.dirKind, this.localConfig);
46
+ }
36
47
  // `--only` narrows the default set, `--skip` then removes from whatever that set is, same
37
48
  // precedence a combined `--only=a,b --skip=b` reads as (mirrors dev-watch's resolveResourceTypes).
38
49
  selectResources(only, skip) {
@@ -6,7 +6,7 @@ import { isForbiddenError, isNotFoundError } from '../lib/wp-client.js';
6
6
  import { diagnoseWpSite } from '../lib/wp-site-diagnostic.js';
7
7
  import { pluralize } from '../utils/pluralize.js';
8
8
  export default class Doctor extends LoopressCommand {
9
- static description = 'Diagnose connectivity, plugin and credential problems for the targeted environment';
9
+ static description = 'Diagnose connectivity, plugin, credential and hook problems for the targeted environment';
10
10
  static enableJsonFlag = true;
11
11
  static examples = ['$ lps doctor', '$ lps doctor --env production'];
12
12
  checks = [];
@@ -1,7 +1,7 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
2
  import { FORM_ENDPOINT, getFormId, getFormTitle } from '../../utils/form-format.js';
3
3
  export default class List extends LoopressCommand {
4
- static description = 'List forms from WordPress';
4
+ static description = 'List forms currently on WordPress';
5
5
  static enableJsonFlag = true;
6
6
  static examples = ['$ lps form list'];
7
7
  async run() {
@@ -10,7 +10,7 @@ export default class Pull extends LoopressCommand {
10
10
  static args = {
11
11
  path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
12
12
  };
13
- static description = 'Pull forms from WordPress';
13
+ static description = 'Pull forms from WordPress into local files';
14
14
  static enableJsonFlag = true;
15
15
  static examples = ['$ lps form pull'];
16
16
  static flags = {
@@ -1,5 +1,6 @@
1
- import { PushCommand } from '../../lib/push-command.js';
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
2
  type FormPushResult = {
3
+ changes?: PushChange[];
3
4
  path: string;
4
5
  status: 'dry-run' | 'success';
5
6
  };
@@ -13,7 +13,7 @@ export default class Push extends PushCommand {
13
13
  static args = {
14
14
  path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
15
15
  };
16
- static description = 'Push forms to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>.json` convention.';
16
+ static description = 'Push local form files to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>.json` convention.';
17
17
  static enableJsonFlag = true;
18
18
  static examples = ['$ lps form push', '$ lps form push --allow-notifications'];
19
19
  static flags = {
@@ -42,7 +42,7 @@ export default class Push extends PushCommand {
42
42
  this.error(`${pluralize(this.failedCount, 'form')} failed to push.`);
43
43
  }
44
44
  if (this.dryRun)
45
- return { path, status: 'dry-run' };
45
+ return { changes: this.previewChanges, path, status: 'dry-run' };
46
46
  await this.recordSuccess();
47
47
  this.log('All forms pushed.');
48
48
  return { path, status: 'success' };
@@ -5,12 +5,6 @@ import { join } from 'node:path';
5
5
  import { configManager } from '../config/project-config.manager.js';
6
6
  import { isInteractive } from '../lib/interactive.js';
7
7
  import { ensureLfGitattributes, writeLocalConfig } from '../utils/loopress-config.js';
8
- // WordPress.org slugs for the two snippet plugins the Loopress WordPress plugin supports
9
- // (see SnippetModule.php, which wires up both providers and auto-detects the active one).
10
- const SNIPPET_PROVIDERS = [
11
- { name: 'Code Snippets', slug: 'code-snippets' },
12
- { name: 'WPCode', slug: 'insert-headers-and-footers' },
13
- ];
14
8
  export default class Init extends Command {
15
9
  static description = 'Initialize a loopress.json config file in the current directory';
16
10
  static examples = ['$ lps init'];
@@ -35,31 +29,14 @@ export default class Init extends Command {
35
29
  default: '.',
36
30
  message: 'Root directory',
37
31
  });
38
- const providerChoice = await select({
39
- choices: [...SNIPPET_PROVIDERS.map((p) => ({ name: p.name, value: p.slug })), { name: 'None / already installed', value: '__none__' }],
40
- message: 'Snippet provider',
41
- });
42
32
  // Resource directories are left out on purpose: every command falls back to its default
43
33
  // (utils/resource-dirs.ts), and a custom name is a one-line edit in loopress.json.
44
34
  const config = { projectId, rootDir };
45
35
  await writeLocalConfig(config);
46
36
  const isGitattributesUpdated = await ensureLfGitattributes();
47
- let isProviderAdded = false;
48
- if (providerChoice !== '__none__') {
49
- try {
50
- await this.config.runCommand('plugin:add', [providerChoice]);
51
- isProviderAdded = true;
52
- }
53
- catch (error) {
54
- this.warn(error.message);
55
- }
56
- }
57
37
  this.log(`\n✓ loopress.json created`);
58
38
  this.log(` Project: ${projectLabel}`);
59
39
  this.log(` Root: ${rootDir}`);
60
- if (isProviderAdded) {
61
- this.log(` Plugin: ${providerChoice}`);
62
- }
63
40
  if (isGitattributesUpdated) {
64
41
  this.log('✓ .gitattributes: LF line endings, so files match across macOS, Linux and Windows');
65
42
  }
@@ -1,5 +1,6 @@
1
- import { PushCommand } from '../../lib/push-command.js';
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
2
  type MenuPushResult = {
3
+ changes?: PushChange[];
3
4
  path: string;
4
5
  status: 'dry-run' | 'success';
5
6
  };
@@ -36,7 +36,7 @@ export default class Push extends PushCommand {
36
36
  this.error(`${pluralize(this.failedCount, 'menu')} failed to push.`);
37
37
  }
38
38
  if (this.dryRun)
39
- return { path, status: 'dry-run' };
39
+ return { changes: this.previewChanges, path, status: 'dry-run' };
40
40
  await this.recordSuccess();
41
41
  this.log('All nav menus pushed.');
42
42
  return { path, status: 'success' };
@@ -1,5 +1,6 @@
1
- import { PushCommand } from '../../lib/push-command.js';
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
2
  type PushResult = {
3
+ changes?: PushChange[];
3
4
  pushed: string[];
4
5
  skipped: string[];
5
6
  status: 'dry-run' | 'success';
@@ -50,7 +50,7 @@ export default class Push extends PushCommand {
50
50
  }
51
51
  const skippedNames = skipped.map((option) => option.name);
52
52
  if (this.dryRun)
53
- return { pushed, skipped: skippedNames, status: 'dry-run' };
53
+ return { changes: this.previewChanges, pushed, skipped: skippedNames, status: 'dry-run' };
54
54
  await this.recordSuccess();
55
55
  this.log('All options pushed.');
56
56
  return { pushed, skipped: skippedNames, status: 'success' };
@@ -1,5 +1,6 @@
1
- import { PushCommand } from '../../lib/push-command.js';
1
+ import { type PushChange, PushCommand } from '../../lib/push-command.js';
2
2
  type PushResult = {
3
+ changes?: PushChange[];
3
4
  pushed: string[];
4
5
  status: 'dry-run' | 'success';
5
6
  };
@@ -14,7 +15,9 @@ export default class Push extends PushCommand {
14
15
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
16
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
17
  };
18
+ private readonly unmanaged;
17
19
  run(): Promise<PushResult>;
20
+ private adoptUnmanaged;
18
21
  private pushPage;
19
22
  }
20
23
  export {};
@@ -1,6 +1,9 @@
1
+ import { confirm } from '@inquirer/prompts';
1
2
  import { Args } from '@oclif/core';
3
+ import { isInteractive } from '../../lib/interactive.js';
2
4
  import { PushCommand } from '../../lib/push-command.js';
3
5
  import { getResourceStateProvider } from '../../lib/resource-state.js';
6
+ import { isConflictCode } from '../../lib/wp-client.js';
4
7
  import { formatPageProblems, frontPageNote, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
5
8
  import { pluralize } from '../../utils/pluralize.js';
6
9
  import { resolveResourceDir } from '../../utils/resource-dirs.js';
@@ -8,13 +11,15 @@ export default class Push extends PushCommand {
8
11
  static args = {
9
12
  slug: Args.string({ description: 'Push only this page (its file name without .html). Pushes every page when omitted.' }),
10
13
  };
11
- static description = 'Push static HTML pages (pages/<slug>.html) to WordPress. The `status` header (draft or publish, draft by default) is applied on every push.';
14
+ static description = 'Push local static HTML pages (pages/<slug>.html) to WordPress, or only one when a slug is given. The `status` header (draft or publish, draft by default) is applied on every push.';
12
15
  static enableJsonFlag = true;
13
- static examples = ['$ lps page push', '$ lps page push legal-notice', '$ lps page push --dry-run'];
16
+ static examples = ['$ lps page push', '$ lps page push legal-notice', '$ lps page push --dry-run', '$ lps page push about --overwrite'];
14
17
  static flags = {
15
18
  ...PushCommand.dryRunFlag,
16
19
  ...PushCommand.yesFlag,
17
20
  };
21
+ // Pages the site refused because a same-slug page exists there that Loopress didn't create.
22
+ unmanaged = [];
18
23
  async run() {
19
24
  const { args } = await this.parse(Push);
20
25
  const path = resolveResourceDir('page', this.localConfig);
@@ -33,31 +38,58 @@ export default class Push extends PushCommand {
33
38
  const beforeState = await this.captureBeforePushState(provider, path, args.slug === undefined ? undefined : (id) => id === args.slug);
34
39
  const pushed = [];
35
40
  await this.runPushTasks(selected, (page) => page.slug, async (page, task) => {
36
- await this.pushPage(page, task);
37
- pushed.push(page.slug);
41
+ if (await this.pushPage(page, task))
42
+ pushed.push(page.slug);
38
43
  });
44
+ await this.adoptUnmanaged(pushed);
39
45
  await this.writeAfterPushSnapshot(provider, path, beforeState);
40
46
  if (this.failedCount > 0) {
41
47
  this.error(`${pluralize(this.failedCount, 'page')} failed to push.`);
42
48
  }
43
49
  if (this.dryRun)
44
- return { pushed, status: 'dry-run' };
50
+ return { changes: this.previewChanges, pushed, status: 'dry-run' };
45
51
  await this.recordSuccess();
46
52
  this.log('All pages pushed.');
47
53
  return { pushed, status: 'success' };
48
54
  }
49
- async pushPage(page, task) {
55
+ // One prompt for every page the first pass set aside, then a second pass that takes them over.
56
+ // Without a terminal to ask in, they count as failed: CI must opt in with --overwrite.
57
+ async adoptUnmanaged(pushed) {
58
+ if (this.unmanaged.length === 0)
59
+ return;
60
+ const slugs = this.unmanaged.map((page) => page.slug);
61
+ this.warn(`Already on ${this.siteConfig.url} but not created by Loopress: ${slugs.join(', ')}. ` +
62
+ 'Taking them over keeps their URL and ID, and shows your file instead of their current content (saved as a WordPress revision first). ' +
63
+ 'Pass --overwrite to take them over.');
64
+ if (!isInteractive() || !(await confirm({ default: false, message: `Take over ${pluralize(slugs.length, 'page')}?` }))) {
65
+ this.failedCount += slugs.length;
66
+ return;
67
+ }
68
+ await this.runPushTasks(this.unmanaged, (page) => page.slug, async (page, task) => {
69
+ if (await this.pushPage(page, task, true))
70
+ pushed.push(page.slug);
71
+ });
72
+ }
73
+ // Resolves false when the page was set aside for adoptUnmanaged instead of pushed.
74
+ async pushPage(page, task, adopt = this.overwrite) {
50
75
  if (this.dryRun) {
51
76
  if (task)
52
77
  task.output = `[dry-run] Would push: ${page.slug} (${page.status})`;
53
- return;
78
+ return true;
54
79
  }
55
80
  try {
56
- const result = await this.wp.put(PAGES_ENDPOINT, page);
81
+ const result = await this.wp.put(PAGES_ENDPOINT, adopt ? { ...page, adopt } : page);
57
82
  if (task)
58
83
  task.output = `Pushed: ${page.slug} (${result.status}) ${result.link}${frontPageNote(result)}`;
84
+ return true;
59
85
  }
60
86
  catch (error) {
87
+ if (!adopt && isConflictCode(error, 'loopress_page_not_managed')) {
88
+ this.unmanaged.push(page);
89
+ // skip(): Listr's own "skipped" mark, not the green check of a page that was pushed.
90
+ task?.skip?.(`${page.slug} already exists on the site, not created by Loopress`);
91
+ return false;
92
+ }
61
93
  this.reportTaskFailure(`Failed to push ${page.slug}: ${error.message}`, error, task);
62
94
  }
63
95
  }
@@ -1,4 +1,4 @@
1
1
  import { declaredPushCommand } from '../../lib/declared-type-command.js';
2
2
  import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
3
3
  export default declaredPushCommand(PATTERN_SPEC, 'Push local block patterns (patterns/<slug>.html, block markup under a title/categories header) to WordPress, ' +
4
- 'registered as loopress/<slug>. Create or update only, never deletes a pattern (use `lps pattern rm`).');
4
+ 'registered as loopress/<slug> and referenced with <!-- wp:pattern {"slug":"loopress/<slug>"} /-->. Create or update only, never deletes a pattern (use `lps pattern rm`).');
@@ -1,3 +1,3 @@
1
1
  import { declaredRmCommand } from '../../lib/declared-type-command.js';
2
2
  import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
3
- export default declaredRmCommand(PATTERN_SPEC, 'Stop registering a Loopress block pattern on WordPress. A wp:pattern reference to it renders nothing, copies already inserted in posts stay.', 'A wp:pattern reference to it renders nothing until it is pushed again, copies already inserted in posts stay.');
3
+ export default declaredRmCommand(PATTERN_SPEC, 'Stop registering a Loopress block pattern on WordPress. A wp:pattern reference to it renders nothing until it is pushed again, copies already inserted in posts stay.', 'A wp:pattern reference to it renders nothing until it is pushed again, copies already inserted in posts stay.');
@@ -6,7 +6,7 @@ import { compareVersions } from '../../utils/version.js';
6
6
  const WPVULN_BASE = 'https://www.wpvulnerability.net/plugin';
7
7
  const WPORG_INFO = 'https://api.wordpress.org/plugins/info/1.2/';
8
8
  export default class Audit extends LoopressCommand {
9
- static description = 'Check the plugins in loopress.json for known vulnerabilities and health issues';
9
+ static description = 'Check the plugins in loopress.json for known vulnerabilities (wpvulnerability.net) and health issues';
10
10
  static enableJsonFlag = true;
11
11
  static examples = ['$ lps plugin audit'];
12
12
  static flags = {};
@@ -20,7 +20,7 @@ const IN_SYNC = {
20
20
  status: 'in-sync',
21
21
  };
22
22
  export default class Push extends PushCommand {
23
- static description = 'Install plugins on WordPress to match loopress.json (or composer.json), via Composer + WPackagist';
23
+ static description = 'Install WordPress.org plugins on WordPress to match loopress.json (active state included) or composer.json, via Composer + WPackagist';
24
24
  static enableJsonFlag = true;
25
25
  static examples = ['$ lps plugin push', '$ lps plugin push --dry-run', '$ lps plugin push --force --prune'];
26
26
  static flags = {
@@ -4,7 +4,7 @@ import { LoopressCommand } from '../../lib/base.js';
4
4
  import { isNotFoundError } from '../../lib/wp-client.js';
5
5
  import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
6
6
  export default class Status extends LoopressCommand {
7
- static description = 'Compare the plugins on WordPress against loopress.json and report drift';
7
+ static description = 'Compare the plugins on WordPress against loopress.json and report drift (missing, wrong version, inactive, untracked)';
8
8
  static enableJsonFlag = true;
9
9
  static examples = ['$ lps plugin status'];
10
10
  static flags = {};
@@ -18,6 +18,6 @@ export default class Pull extends LoopressCommand {
18
18
  private failedCount;
19
19
  run(): Promise<PullResult>;
20
20
  private buildArgv;
21
- private usesResource;
21
+ private globalStylesSkipNote;
22
22
  }
23
23
  export {};
@@ -1,12 +1,9 @@
1
- import { existsSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
1
  import { LoopressCommand } from '../lib/base.js';
4
2
  import { stdoutToStderr } from '../lib/json-delegation.js';
3
+ import { INTEGRATION_MISSING } from '../lib/plugin-detection.js';
5
4
  import { pluralize } from '../utils/pluralize.js';
6
- import { resolveResourceDir } from '../utils/resource-dirs.js';
7
- // How the plugin reports its integration missing: "ACF is not active", "No supported form
8
- // plugin is active", "No supported snippet plugin is active".
9
- const PLUGIN_MISSING = /\bis not active\b|\bNo supported .+ plugin is active\b/;
5
+ import { projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
6
+ import { globalStylesSkipNote } from '../utils/theme-styles-format.js';
10
7
  // composer before plugins: when a composer.json exists, `plugin:pull` merges the live plugins
11
8
  // into it, so it must run on the freshly pulled one or `composer:pull` would overwrite its work. The rest write to independent
12
9
  // directories and their order does not matter, it mirrors `lps push` for familiarity.
@@ -22,12 +19,15 @@ const PULL_TARGETS = [
22
19
  { commandId: 'hook:pull', label: 'hooks', supportsYes: true },
23
20
  { commandId: 'form:pull', label: 'forms', needsPlugin: 'form', supportsYes: true },
24
21
  { commandId: 'pattern:pull', label: 'patterns', supportsYes: true },
22
+ // Templates and parts have no pull. Global Styles are pulled only once the project tracks them,
23
+ // see globalStylesSkipNote.
24
+ { commandId: 'theme:style:pull', label: 'Global Styles' },
25
25
  { commandId: 'menu:pull', label: 'menus', supportsYes: true },
26
26
  { commandId: 'option:pull', label: 'options', supportsYes: true },
27
27
  { commandId: 'snippet:pull', label: 'snippets', needsPlugin: 'snippets', supportsYes: true },
28
28
  ];
29
29
  export default class Pull extends LoopressCommand {
30
- static description = 'Pull all content, plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, menus, options, and snippets, from WordPress';
30
+ static description = 'Pull every tracked resource from WordPress into local files in one run: composer dependencies, plugins, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, Global Styles, menus, options, and snippets';
31
31
  static enableJsonFlag = true;
32
32
  static examples = ['$ lps pull', '$ lps pull --env staging', '$ lps pull --dry-run'];
33
33
  static flags = {
@@ -39,6 +39,12 @@ export default class Pull extends LoopressCommand {
39
39
  const results = [];
40
40
  this.warnRemovedSeoModule();
41
41
  for (const target of PULL_TARGETS) {
42
+ const skipNote = target.commandId === 'theme:style:pull' ? await this.globalStylesSkipNote() : undefined;
43
+ if (skipNote) {
44
+ this.log(`\n→ ${target.label}: ${skipNote}`);
45
+ results.push({ label: target.label, status: 'skipped' });
46
+ continue;
47
+ }
42
48
  this.log(`\n→ Pulling ${target.label}...`);
43
49
  try {
44
50
  await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand(target.commandId, this.buildArgv(target)));
@@ -50,7 +56,7 @@ export default class Pull extends LoopressCommand {
50
56
  // A site without ACF (or a form or snippet plugin) is the common case, not a failure,
51
57
  // as long as the project doesn't use that resource: a vanilla site must pull, and a
52
58
  // promote must not stop on it. A project that does use it still fails.
53
- if (target.needsPlugin && PLUGIN_MISSING.test(message) && !this.usesResource(target.needsPlugin)) {
59
+ if (target.needsPlugin && INTEGRATION_MISSING.test(message) && !projectUses(target.needsPlugin, this.localConfig)) {
54
60
  // oclif's catch already set exitCode to 1 for the sub-command; a real failure still
55
61
  // exits non-zero through the this.error() below.
56
62
  process.exitCode = undefined;
@@ -82,7 +88,13 @@ export default class Pull extends LoopressCommand {
82
88
  argv.push('--yes');
83
89
  return argv;
84
90
  }
85
- usesResource(kind) {
86
- return existsSync(resolve(resolveResourceDir(kind, this.localConfig)));
91
+ // The site unreachable or the theme list refused: let the step run and report it itself.
92
+ async globalStylesSkipNote() {
93
+ try {
94
+ return await globalStylesSkipNote(this.wp, resolveResourceDir('themeStyles', this.localConfig), 'pull');
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
87
99
  }
88
100
  }
@@ -2,6 +2,7 @@ import { LoopressCommand } from '../lib/base.js';
2
2
  type PushTargetResult = {
3
3
  error?: string;
4
4
  label: string;
5
+ result?: unknown;
5
6
  status: 'failed' | 'pushed' | 'skipped';
6
7
  };
7
8
  type PushResult = {
@@ -20,6 +21,7 @@ export default class Push extends LoopressCommand {
20
21
  private buildArgv;
21
22
  private composerDeclaresPlugins;
22
23
  private composerJsonPath;
24
+ private globalStylesSkipNote;
23
25
  private guardProductionPush;
24
26
  private skippedTargets;
25
27
  }