@loopress/cli 0.29.0 → 0.31.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 (154) hide show
  1. package/README.md +1369 -360
  2. package/dist/commands/acf/push.js +2 -1
  3. package/dist/commands/composer/push.d.ts +1 -0
  4. package/dist/commands/composer/push.js +13 -0
  5. package/dist/commands/content/copy.d.ts +28 -0
  6. package/dist/commands/content/copy.js +97 -0
  7. package/dist/commands/cpt/diff.js +5 -0
  8. package/dist/commands/cpt/list.d.ts +2 -0
  9. package/dist/commands/cpt/list.js +4 -0
  10. package/dist/commands/cpt/pull.d.ts +2 -0
  11. package/dist/commands/cpt/pull.js +4 -0
  12. package/dist/commands/cpt/push.d.ts +2 -0
  13. package/dist/commands/cpt/push.js +4 -0
  14. package/dist/commands/cpt/rm.d.ts +2 -0
  15. package/dist/commands/cpt/rm.js +3 -0
  16. package/dist/commands/cpt/rollback.js +5 -0
  17. package/dist/commands/dev.js +14 -4
  18. package/dist/commands/diff.js +1 -1
  19. package/dist/commands/doctor.d.ts +2 -0
  20. package/dist/commands/doctor.js +57 -2
  21. package/dist/commands/form/push.js +4 -1
  22. package/dist/commands/meta/diff.d.ts +2 -0
  23. package/dist/commands/meta/diff.js +5 -0
  24. package/dist/commands/meta/list.d.ts +19 -0
  25. package/dist/commands/meta/list.js +21 -0
  26. package/dist/commands/meta/pull.d.ts +2 -0
  27. package/dist/commands/meta/pull.js +4 -0
  28. package/dist/commands/meta/push.d.ts +2 -0
  29. package/dist/commands/meta/push.js +4 -0
  30. package/dist/commands/meta/rm.d.ts +2 -0
  31. package/dist/commands/meta/rm.js +3 -0
  32. package/dist/commands/meta/rollback.d.ts +2 -0
  33. package/dist/commands/meta/rollback.js +5 -0
  34. package/dist/commands/option/list.js +2 -8
  35. package/dist/commands/option/push.js +4 -2
  36. package/dist/commands/page/push.js +3 -2
  37. package/dist/commands/pattern/diff.d.ts +2 -0
  38. package/dist/commands/pattern/diff.js +5 -0
  39. package/dist/commands/pattern/list.d.ts +2 -0
  40. package/dist/commands/pattern/list.js +3 -0
  41. package/dist/commands/pattern/pull.d.ts +2 -0
  42. package/dist/commands/pattern/pull.js +4 -0
  43. package/dist/commands/pattern/push.d.ts +2 -0
  44. package/dist/commands/pattern/push.js +4 -0
  45. package/dist/commands/pattern/rm.d.ts +2 -0
  46. package/dist/commands/pattern/rm.js +3 -0
  47. package/dist/commands/pattern/rollback.d.ts +2 -0
  48. package/dist/commands/pattern/rollback.js +5 -0
  49. package/dist/commands/plugin/audit.js +43 -25
  50. package/dist/commands/project/config.d.ts +1 -0
  51. package/dist/commands/project/config.js +20 -1
  52. package/dist/commands/project/list.d.ts +1 -1
  53. package/dist/commands/project/list.js +6 -6
  54. package/dist/commands/project/pull.js +11 -8
  55. package/dist/commands/project/push.js +23 -18
  56. package/dist/commands/project/remove.js +1 -1
  57. package/dist/commands/project/switch.d.ts +0 -1
  58. package/dist/commands/project/switch.js +16 -36
  59. package/dist/commands/promote.d.ts +1 -0
  60. package/dist/commands/promote.js +7 -3
  61. package/dist/commands/pull.d.ts +2 -1
  62. package/dist/commands/pull.js +31 -6
  63. package/dist/commands/push.d.ts +4 -2
  64. package/dist/commands/push.js +51 -19
  65. package/dist/commands/role/diff.d.ts +2 -0
  66. package/dist/commands/role/diff.js +5 -0
  67. package/dist/commands/role/list.d.ts +2 -0
  68. package/dist/commands/role/list.js +4 -0
  69. package/dist/commands/role/pull.d.ts +2 -0
  70. package/dist/commands/role/pull.js +4 -0
  71. package/dist/commands/role/push.d.ts +2 -0
  72. package/dist/commands/role/push.js +4 -0
  73. package/dist/commands/role/rm.d.ts +2 -0
  74. package/dist/commands/role/rm.js +3 -0
  75. package/dist/commands/role/rollback.d.ts +2 -0
  76. package/dist/commands/role/rollback.js +5 -0
  77. package/dist/commands/snippet/push.d.ts +1 -0
  78. package/dist/commands/snippet/push.js +24 -3
  79. package/dist/commands/status.d.ts +2 -3
  80. package/dist/commands/status.js +21 -53
  81. package/dist/commands/taxonomy/diff.d.ts +2 -0
  82. package/dist/commands/taxonomy/diff.js +5 -0
  83. package/dist/commands/taxonomy/list.d.ts +2 -0
  84. package/dist/commands/taxonomy/list.js +5 -0
  85. package/dist/commands/taxonomy/pull.d.ts +2 -0
  86. package/dist/commands/taxonomy/pull.js +4 -0
  87. package/dist/commands/taxonomy/push.d.ts +2 -0
  88. package/dist/commands/taxonomy/push.js +4 -0
  89. package/dist/commands/taxonomy/rm.d.ts +2 -0
  90. package/dist/commands/taxonomy/rm.js +3 -0
  91. package/dist/commands/taxonomy/rollback.d.ts +2 -0
  92. package/dist/commands/taxonomy/rollback.js +5 -0
  93. package/dist/commands/theme/template/push.d.ts +2 -0
  94. package/dist/commands/theme/template/push.js +26 -5
  95. package/dist/commands/theme/version/pull.js +6 -1
  96. package/dist/config/project-config.manager.d.ts +3 -3
  97. package/dist/config/project-config.manager.js +22 -31
  98. package/dist/hooks/finally.js +11 -3
  99. package/dist/lib/api-client.js +2 -1
  100. package/dist/lib/app-build.js +3 -1
  101. package/dist/lib/base.d.ts +2 -2
  102. package/dist/lib/base.js +33 -38
  103. package/dist/lib/declared-type-command.d.ts +35 -0
  104. package/dist/lib/declared-type-command.js +192 -0
  105. package/dist/lib/dev-watch.d.ts +1 -1
  106. package/dist/lib/dev-watch.js +7 -3
  107. package/dist/lib/diff-command.d.ts +2 -0
  108. package/dist/lib/diff-command.js +24 -3
  109. package/dist/lib/github-release.js +4 -0
  110. package/dist/lib/load-snippets.js +4 -0
  111. package/dist/lib/local-callback-server.js +3 -1
  112. package/dist/lib/php-files-command.d.ts +8 -0
  113. package/dist/lib/php-files-command.js +5 -0
  114. package/dist/lib/push-command.d.ts +5 -2
  115. package/dist/lib/push-command.js +96 -15
  116. package/dist/lib/put-or-create.js +3 -4
  117. package/dist/lib/resource-state.js +38 -85
  118. package/dist/lib/rollback-command.js +9 -2
  119. package/dist/lib/rollback-materialize.js +13 -24
  120. package/dist/lib/spinner.d.ts +2 -0
  121. package/dist/lib/spinner.js +72 -0
  122. package/dist/lib/temp-admin.d.ts +10 -1
  123. package/dist/lib/temp-admin.js +27 -2
  124. package/dist/lib/validate-local.js +40 -12
  125. package/dist/lib/wp-client.d.ts +1 -0
  126. package/dist/lib/wp-client.js +21 -2
  127. package/dist/lib/wp-site-diagnostic.js +3 -2
  128. package/dist/types/global-config.generated.d.ts +1 -5
  129. package/dist/types/project-config.generated.d.ts +28 -4
  130. package/dist/types/snippet.d.ts +1 -0
  131. package/dist/utils/composer.js +2 -1
  132. package/dist/utils/declared-type-format.d.ts +52 -0
  133. package/dist/utils/declared-type-format.js +233 -0
  134. package/dist/utils/resource-dirs.d.ts +9 -3
  135. package/dist/utils/resource-dirs.js +16 -4
  136. package/dist/utils/template-format.d.ts +1 -0
  137. package/dist/utils/theme-css.d.ts +19 -0
  138. package/dist/utils/theme-css.js +100 -0
  139. package/oclif.manifest.json +3294 -1213
  140. package/package.json +32 -16
  141. package/schemas/global-config.schema.json +2 -6
  142. package/schemas/project-config.schema.json +37 -7
  143. package/dist/commands/seo/diff.js +0 -5
  144. package/dist/commands/seo/list.d.ts +0 -18
  145. package/dist/commands/seo/list.js +0 -58
  146. package/dist/commands/seo/pull.d.ts +0 -24
  147. package/dist/commands/seo/pull.js +0 -89
  148. package/dist/commands/seo/push.d.ts +0 -29
  149. package/dist/commands/seo/push.js +0 -178
  150. package/dist/commands/seo/rollback.js +0 -5
  151. package/dist/utils/seo-format.d.ts +0 -29
  152. package/dist/utils/seo-format.js +0 -18
  153. /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
  154. /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
@@ -1,47 +1,56 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import { LoopressCommand } from '../lib/base.js';
5
5
  import { guardProductionPush } from '../lib/guard-production-push.js';
6
6
  import { stdoutToStderr } from '../lib/json-delegation.js';
7
+ import { PushCommand } from '../lib/push-command.js';
7
8
  import { isLocalEnvironment } from '../lib/wp-client.js';
8
9
  import { wpackagistRequire } from '../utils/composer.js';
9
- import { pluralize } from '../utils/pluralize.js';
10
+ import { resolveResourceDir } from '../utils/resource-dirs.js';
10
11
  // Dependency-ish order: code first (plugins, composer), then the ACF definitions content
11
12
  // relies on, then content itself.
12
13
  const PUSH_TARGETS = [
13
14
  { commandId: 'plugin:push', label: 'plugins' },
14
15
  { commandId: 'composer:push', label: 'composer' },
16
+ // Before ACF: a field group's location rule can target a Loopress post type or taxonomy.
17
+ { commandId: 'cpt:push', label: 'post types' },
18
+ { commandId: 'taxonomy:push', label: 'taxonomies' },
19
+ { commandId: 'meta:push', label: 'post meta' },
20
+ { commandId: 'role:push', label: 'roles' },
15
21
  { commandId: 'acf:push', label: 'ACF' },
16
22
  { commandId: 'api:push', label: 'API routes' },
17
23
  { commandId: 'hook:push', label: 'hooks' },
18
24
  { commandId: 'form:push', label: 'forms' },
25
+ // Before templates and pages, which can reference them (`wp:pattern`).
26
+ { commandId: 'pattern:push', label: 'patterns' },
19
27
  // Before pages: WordPress refuses a page whose `template` it doesn't know yet.
20
28
  { commandId: 'theme:template:push', label: 'templates and parts' },
21
29
  { commandId: 'page:push', label: 'pages' },
22
- { commandId: 'seo:push', label: 'SEO' },
23
30
  { commandId: 'menu:push', label: 'menus' },
24
31
  { commandId: 'option:push', label: 'options' },
25
32
  { commandId: 'snippet:push', label: 'snippets' },
26
33
  ];
27
34
  export default class Push extends LoopressCommand {
28
- static description = 'Push all local content, plugins, composer dependencies, ACF, API routes, hooks, forms, templates, pages, SEO, menus, options, and snippets, to WordPress';
35
+ static description = 'Push all local content, plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, templates, pages, menus, options, and snippets, to WordPress';
29
36
  static enableJsonFlag = true;
30
37
  static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run'];
31
38
  static flags = {
32
39
  ...LoopressCommand.dryRunFlag,
33
40
  ...LoopressCommand.yesFlag,
41
+ overwrite: PushCommand.baseFlags.overwrite,
34
42
  };
35
- failedCount = 0;
36
43
  async run() {
37
44
  await this.guardProductionPush();
45
+ this.warnRemovedSeoModule();
38
46
  const argv = this.buildArgv();
39
47
  const results = [];
40
- const composerPushedByPlugins = await this.composerDeclaresPlugins();
48
+ const skips = await this.skippedTargets();
41
49
  for (const target of PUSH_TARGETS) {
42
- if (composerPushedByPlugins && target.commandId === 'composer:push') {
43
- this.log('\n→ composer: already pushed whole by plugins');
44
- results.push({ label: target.label, status: 'pushed' });
50
+ const skip = skips.get(target.commandId);
51
+ if (skip) {
52
+ this.log(`\n→ ${target.label}: ${skip.note}`);
53
+ results.push({ label: target.label, status: skip.status });
45
54
  continue;
46
55
  }
47
56
  this.log(`\n→ Pushing ${target.label}...`);
@@ -51,20 +60,16 @@ export default class Push extends LoopressCommand {
51
60
  results.push({ label: target.label, status: 'pushed' });
52
61
  }
53
62
  catch (error) {
54
- this.failedCount++;
55
63
  const { message } = error;
56
64
  this.log(`✗ ${target.label} failed: ${message}`);
57
65
  results.push({ error: message, label: target.label, status: 'failed' });
66
+ // Stop here: later targets depend on earlier ones (hooks use what composer installs,
67
+ // pages need their templates), so pushing them anyway can ship a silently broken site.
68
+ const skipped = PUSH_TARGETS.slice(PUSH_TARGETS.indexOf(target) + 1).map((next) => next.label);
69
+ const skippedNote = skipped.length > 0 ? ` Not attempted: ${skipped.join(', ')}.` : '';
70
+ this.error(`${target.label} failed to push: ${message}.${skippedNote}`);
58
71
  }
59
72
  }
60
- if (this.failedCount > 0) {
61
- // Each failure's own reason, not just the count: under --json the per-resource lines above
62
- // never reach stdout, and this message is all an MCP caller or a script gets to see.
63
- const failures = results
64
- .filter((result) => result.status === 'failed')
65
- .map((result) => `${result.label}: ${result.error}`);
66
- this.error(`${pluralize(this.failedCount, 'resource')} failed to push. ${failures.join('; ')}`);
67
- }
68
73
  this.log(this.dryRun ? '\nEvery resource would push.' : '\nAll resources pushed.');
69
74
  return { results };
70
75
  }
@@ -75,12 +80,15 @@ export default class Push extends LoopressCommand {
75
80
  const argv = ['--env', this.siteConfig.name, '--yes'];
76
81
  if (this.dryRun)
77
82
  argv.push('--dry-run');
83
+ // Not implied by --yes: overwriting changes made outside Loopress is its own decision.
84
+ if (this.overwrite)
85
+ argv.push('--overwrite');
78
86
  return argv;
79
87
  }
80
88
  // A composer.json that declares plugins is pushed whole by `plugin:push` (with its activation
81
89
  // safety), so `composer:push` would only run Composer on the server a second time.
82
90
  async composerDeclaresPlugins() {
83
- const path = join(process.cwd(), this.rootDir, 'composer.json');
91
+ const path = this.composerJsonPath();
84
92
  if (!existsSync(path))
85
93
  return false;
86
94
  try {
@@ -92,6 +100,9 @@ export default class Push extends LoopressCommand {
92
100
  return false;
93
101
  }
94
102
  }
103
+ composerJsonPath() {
104
+ return join(process.cwd(), this.rootDir, 'composer.json');
105
+ }
95
106
  // Guards once here (shared with PushCommand, lib/push-command.ts) rather than once per
96
107
  // delegated command.
97
108
  async guardProductionPush() {
@@ -102,4 +113,25 @@ export default class Push extends LoopressCommand {
102
113
  yes: this.yes,
103
114
  });
104
115
  }
116
+ // Unconfigured is not a failure: a project that doesn't manage plugins or composer through
117
+ // Loopress must not stop the run before anything else gets pushed.
118
+ async skippedTargets() {
119
+ const skips = new Map();
120
+ const notConfigured = { note: 'not configured, skipped', status: 'skipped' };
121
+ const hasComposerJson = existsSync(this.composerJsonPath());
122
+ const composerPushedByPlugins = await this.composerDeclaresPlugins();
123
+ if (composerPushedByPlugins)
124
+ skips.set('composer:push', { note: 'already pushed whole by plugins', status: 'pushed' });
125
+ else if (!hasComposerJson)
126
+ skips.set('composer:push', notConfigured);
127
+ // With a composer.json, plugins come from it only; otherwise from loopress.json.
128
+ const pluginsConfigured = hasComposerJson ? composerPushedByPlugins : Object.keys(this.localConfig.plugins ?? {}).length > 0;
129
+ if (!pluginsConfigured)
130
+ skips.set('plugin:push', notConfigured);
131
+ // The only push that fails on a missing directory (the others find 0 files), so a project
132
+ // that never used snippets would otherwise end every `lps push` on an ENOENT.
133
+ if (!existsSync(resolve(resolveResourceDir('snippets', this.localConfig))))
134
+ skips.set('snippet:push', notConfigured);
135
+ return skips;
136
+ }
105
137
  }
@@ -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('role', {
3
+ description: 'Show what differs in roles between your local roles/ files and a WordPress environment, or between two environments',
4
+ pathNoun: 'roles directory',
5
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredListCommand>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { declaredListCommand } from '../../lib/declared-type-command.js';
2
+ import { ROLE_SPEC } from '../../utils/declared-type-format.js';
3
+ import { pluralize } from '../../utils/pluralize.js';
4
+ export default declaredListCommand(ROLE_SPEC, 'List the user roles on WordPress, where each comes from (Loopress, WordPress, a plugin) and how many users have it. Only Loopress ones are pulled and pushed.', (count) => pluralize(count, 'user'));
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredPullCommand>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { declaredPullCommand } from '../../lib/declared-type-command.js';
2
+ import { ROLE_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredPullCommand(ROLE_SPEC, 'Pull the roles Loopress manages on WordPress into roles/<slug>.json files. ' +
4
+ 'Built-in roles and roles created by a plugin are never pulled, see `lps role list`.');
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredPushCommand>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { declaredPushCommand } from '../../lib/declared-type-command.js';
2
+ import { ROLE_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredPushCommand(ROLE_SPEC, 'Push local roles (roles/<slug>.json, a "name" and its "capabilities") to WordPress. ' +
4
+ 'Create or update only, never deletes a role (use `lps role rm`), never changes which users have it.');
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredRmCommand>;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { declaredRmCommand } from '../../lib/declared-type-command.js';
2
+ import { ROLE_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredRmCommand(ROLE_SPEC, 'Stop registering a Loopress role on WordPress. Its users keep it in their profile, with no capability until the role is pushed again.', 'Its users keep it in their profile, with no capability until it is pushed again.');
@@ -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('role', {
3
+ description: 'Restore roles to a snapshot taken automatically before an earlier `lps role push`',
4
+ pathNoun: 'roles directory',
5
+ });
@@ -24,5 +24,6 @@ export default class Push extends PushCommand {
24
24
  private loadSnippets;
25
25
  private pushSnippet;
26
26
  private toPayload;
27
+ private withoutDuplicateIds;
27
28
  }
28
29
  export {};
@@ -28,7 +28,7 @@ export default class Push extends PushCommand {
28
28
  this.log(`Snippets path: ${path}`);
29
29
  const provider = getResourceStateProvider('snippet');
30
30
  const beforeState = await this.captureBeforePushState(provider, path);
31
- const snippets = await this.loadSnippets(path);
31
+ const snippets = this.withoutDuplicateIds(await this.loadSnippets(path));
32
32
  this.log(`Found ${pluralize(snippets.length, 'snippet')} to push`);
33
33
  const pushed = [];
34
34
  await this.runPushTasks(snippets, (snippet) => snippet.name, async (snippet, task) => {
@@ -148,8 +148,11 @@ export default class Push extends PushCommand {
148
148
  toPayload(snippet) {
149
149
  return {
150
150
  active: snippet.active,
151
- code: stripPhpOpeningTag(snippet.code),
152
- description: `Imported from ${snippet.path}`,
151
+ // Only PHP files carry an editor-only `<?php` tag: an HTML snippet starting with an inline
152
+ // `<?php ... ?>` block is its real content and must reach WordPress untouched.
153
+ code: snippet.type === 'php' ? stripPhpOpeningTag(snippet.code) : snippet.code,
154
+ // The sidecar's description wins, so a pull then push never erases one written in wp-admin.
155
+ description: snippet.description ?? `Imported from ${snippet.path}`,
153
156
  insertMethod: snippet.insertMethod,
154
157
  location: snippet.location,
155
158
  name: snippet.name,
@@ -159,4 +162,22 @@ export default class Push extends PushCommand {
159
162
  type: snippet.type,
160
163
  };
161
164
  }
165
+ // Two files claiming the same id would each PUT over that id, the last one silently replacing
166
+ // the other on the site (seen after switching snippet provider, whose ids restart). Neither is
167
+ // pushed, and the push fails, until the files are fixed by hand.
168
+ withoutDuplicateIds(snippets) {
169
+ const pathsById = new Map();
170
+ for (const { id, path } of snippets) {
171
+ if (id !== undefined)
172
+ pathsById.set(id, [...(pathsById.get(id) ?? []), path]);
173
+ }
174
+ return snippets.filter((snippet) => {
175
+ const paths = snippet.id === undefined ? [] : pathsById.get(snippet.id);
176
+ if (paths.length < 2)
177
+ return true;
178
+ this.failedCount++;
179
+ this.warn(`Skipping "${snippet.path}": id ${snippet.id} is also used by ${paths.filter((p) => p !== snippet.path).join(', ')}.`);
180
+ return false;
181
+ });
182
+ }
162
183
  }
@@ -18,8 +18,7 @@ export default class Status extends Command {
18
18
  env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
19
  };
20
20
  run(): Promise<StatusResult>;
21
- private reportActiveProject;
22
- private reportEnvOverride;
23
- private reportPinnedProject;
21
+ private logTarget;
22
+ private report;
24
23
  }
25
24
  export {};
@@ -11,52 +11,26 @@ export default class Status extends Command {
11
11
  };
12
12
  async run() {
13
13
  const { flags } = await this.parse(Status);
14
- const localConfig = await readLocalConfig();
15
- let info;
16
- if (flags.env) {
17
- info = this.reportEnvOverride(localConfig.projectId, flags.env);
18
- }
19
- else if (localConfig.projectId) {
20
- info = this.reportPinnedProject(localConfig.projectId);
21
- }
22
- else {
23
- info = this.reportActiveProject();
24
- }
14
+ const { projectId } = await readLocalConfig();
15
+ const info = this.report(projectId, flags.env);
25
16
  this.log('');
26
17
  this.log(`Config dir: ${this.config.configDir}`);
27
18
  this.log(`Data dir: ${this.config.dataDir}`);
28
19
  return { ...info, configDir: this.config.configDir, dataDir: this.config.dataDir };
29
20
  }
30
- reportActiveProject() {
31
- const env = configManager.getCurrentEnv();
32
- const project = env ? configManager.getCurrentProject() : undefined;
33
- if (!env || !project) {
34
- const note = 'No project configured. Run `lps project config` first.';
35
- this.log(note);
36
- return { note };
37
- }
38
- this.log(`Project: ${project.name} (${env.name})`);
21
+ logTarget(label, env) {
22
+ this.log(`Project: ${label}`);
39
23
  this.log(`URL: ${env.url}`);
40
- return { environment: env.name, project: `${project.name} (${env.name})`, url: env.url };
24
+ return { environment: env.name, project: label, url: env.url };
41
25
  }
42
- // Mirrors base.ts:resolveEnvironment with --env: the targeted project comes from
43
- // loopress.json when pinned, from the globally active project otherwise.
44
- reportEnvOverride(pinnedProjectId, envName) {
45
- const project = pinnedProjectId ? configManager.getProject(pinnedProjectId) : configManager.getCurrentProject();
46
- if (!project) {
26
+ // Mirrors base.ts:resolveEnvironment, reporting instead of failing where it can.
27
+ report(pinnedProjectId, envName) {
28
+ const projectId = pinnedProjectId ?? configManager.getCurrentProject()?.id;
29
+ if (!projectId) {
47
30
  const note = 'No project configured. Run `lps project config` first.';
48
31
  this.log(note);
49
32
  return { note };
50
33
  }
51
- const env = project.environments[envName];
52
- if (!env) {
53
- this.error(`Environment "${envName}" not found in project "${project.name}". Available: ${Object.keys(project.environments).join(', ')}`);
54
- }
55
- this.log(`Project: ${project.name} (${env.name}, via --env)`);
56
- this.log(`URL: ${env.url}`);
57
- return { environment: env.name, project: `${project.name} (${env.name}, via --env)`, url: env.url };
58
- }
59
- reportPinnedProject(projectId) {
60
34
  const project = configManager.getProject(projectId);
61
35
  if (!project) {
62
36
  const note = `loopress.json pins project "${projectId}", but it no longer exists. Run \`lps project config\` to configure it.`;
@@ -71,28 +45,22 @@ export default class Status extends Command {
71
45
  this.log(note);
72
46
  return { note, project: project.name };
73
47
  }
74
- if (envNames.length === 1) {
75
- const env = project.environments[envNames[0]];
76
- this.log(`Project: ${project.name} (${env.name})`);
77
- this.log(`URL: ${env.url}`);
78
- return { environment: env.name, project: `${project.name} (${env.name})`, url: env.url };
48
+ if (envName) {
49
+ const env = project.environments[envName];
50
+ if (!env) {
51
+ this.error(`Environment "${envName}" not found in project "${project.name}". Available: ${envNames.join(', ')}`);
52
+ }
53
+ return this.logTarget(`${project.name} (${env.name}, via --env)`, env);
79
54
  }
80
- const current = configManager.getCurrentProject();
81
- const currentEnv = current?.id === projectId ? configManager.getCurrentEnv() : null;
82
- if (!currentEnv) {
83
- const note = `"${project.name}" has multiple environments and isn't the globally active project. Run \`lps project switch\` to pick one.`;
84
- this.log(`Project: ${project.name} ${c('yellow', '(ambiguous)')}`);
55
+ const env = configManager.getDefaultEnvironment(projectId);
56
+ if (!env) {
57
+ const note = `"${project.name}" has no "local" environment, pass --env to pick one.`;
58
+ this.log(`Project: ${project.name} ${c('yellow', '(no default environment)')}`);
85
59
  this.log(`Environments: ${envNames.join(', ')}`);
86
60
  this.log('');
87
- this.warn(`"${project.name}" has multiple environments and isn't the globally active project.`);
88
- this.log('Run `lps project switch` to pick one before running commands here.');
89
- if (current) {
90
- this.log(`(Globally active project right now: "${current.name}")`);
91
- }
61
+ this.warn(note);
92
62
  return { environments: envNames, note, project: project.name };
93
63
  }
94
- this.log(`Project: ${project.name} (${currentEnv.name})`);
95
- this.log(`URL: ${currentEnv.url}`);
96
- return { environment: currentEnv.name, project: `${project.name} (${currentEnv.name})`, url: currentEnv.url };
64
+ return this.logTarget(`${project.name} (${env.name})`, env);
97
65
  }
98
66
  }
@@ -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('taxonomy', {
3
+ description: 'Show what differs in taxonomies between your local taxonomies/ files and a WordPress environment, or between two environments',
4
+ pathNoun: 'taxonomies directory',
5
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredListCommand>;
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { declaredListCommand } from '../../lib/declared-type-command.js';
2
+ import { TAXONOMY_SPEC } from '../../utils/declared-type-format.js';
3
+ import { pluralize } from '../../utils/pluralize.js';
4
+ export default declaredListCommand(TAXONOMY_SPEC, 'List the taxonomies registered on WordPress, where each comes from (Loopress, WordPress, ACF, CPT UI, a theme or plugin) ' +
5
+ 'and the post types it attaches to. Only Loopress ones are pulled and pushed.', (count) => pluralize(count, 'term'));
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredPullCommand>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { declaredPullCommand } from '../../lib/declared-type-command.js';
2
+ import { TAXONOMY_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredPullCommand(TAXONOMY_SPEC, 'Pull the taxonomies Loopress manages on WordPress into taxonomies/<slug>.json files, never their terms. ' +
4
+ 'Taxonomies registered by a theme or another plugin are never pulled, see `lps taxonomy list`.');
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredPushCommand>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { declaredPushCommand } from '../../lib/declared-type-command.js';
2
+ import { TAXONOMY_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredPushCommand(TAXONOMY_SPEC, 'Push local taxonomies (taxonomies/<slug>.json, register_taxonomy() arguments plus "object_type") to WordPress. ' +
4
+ 'Create or update only, never deletes a taxonomy (use `lps taxonomy rm`), never touches its terms.');
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/declared-type-command.js").DeclaredRmCommand>;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { declaredRmCommand } from '../../lib/declared-type-command.js';
2
+ import { TAXONOMY_SPEC } from '../../utils/declared-type-format.js';
3
+ export default declaredRmCommand(TAXONOMY_SPEC, 'Stop registering a Loopress taxonomy on WordPress. Its terms are kept in the database, hidden until the taxonomy is pushed again.', "Its terms are kept in the database, hidden until it's pushed again.");
@@ -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('taxonomy', {
3
+ description: 'Restore taxonomies to a snapshot taken automatically before an earlier `lps taxonomy push`',
4
+ pathNoun: 'taxonomies directory',
5
+ });
@@ -1,11 +1,13 @@
1
1
  import { PushCommand } from '../../../lib/push-command.js';
2
2
  type PushResult = {
3
3
  active?: boolean;
4
+ blockStyles: string[];
4
5
  customized?: string[];
5
6
  parts: string[];
6
7
  status: 'dry-run' | 'success';
7
8
  stylesheet?: string;
8
9
  templates: string[];
10
+ unknownBlocks?: string[];
9
11
  };
10
12
  export default class Push extends PushCommand {
11
13
  static description: string;
@@ -4,8 +4,9 @@ import { formatPageProblems } from '../../../utils/page-format.js';
4
4
  import { pluralize } from '../../../utils/pluralize.js';
5
5
  import { resolveResourceDir } from '../../../utils/resource-dirs.js';
6
6
  import { CHILD_THEME_ENDPOINT, readLocalTemplates } from '../../../utils/template-format.js';
7
+ import { bundleThemeCss } from '../../../utils/theme-css.js';
7
8
  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 description = 'Push block templates (theme/templates/<slug>.html), template parts (theme/parts/<slug>.html) and the theme CSS (theme/css/style.css, theme/blocks/<namespace>/<block>.css, bundled with esbuild) 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
10
  static enableJsonFlag = true;
10
11
  static examples = ['$ lps theme template push', '$ lps theme template push --dry-run'];
11
12
  static flags = {
@@ -19,17 +20,24 @@ export default class Push extends PushCommand {
19
20
  this.log(`Pushing templates and parts to ${this.siteConfig.url}`);
20
21
  this.log(`Templates path: ${templatesPath}, parts path: ${partsPath}`);
21
22
  const { parts, problems, templates } = await readLocalTemplates(templatesPath, partsPath);
23
+ const { css, problems: cssProblems } = await bundleThemeCss(resolveResourceDir('css', this.localConfig), resolveResourceDir('blockCss', this.localConfig));
24
+ problems.push(...cssProblems);
22
25
  if (problems.length > 0) {
23
26
  this.error(`Nothing was pushed, fix these files first:\n ${formatPageProblems(problems)}`);
24
27
  }
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')}`);
28
+ const names = {
29
+ blockStyles: css.blocks.map((block) => block.name),
30
+ parts: parts.map((part) => part.slug),
31
+ templates: templates.map((template) => template.slug),
32
+ };
33
+ this.log(`Found ${pluralize(templates.length, 'template')}, ${pluralize(parts.length, 'part')}, ` +
34
+ `${css.style ? 'a global stylesheet' : 'no global stylesheet'} and ${pluralize(css.blocks.length, 'block stylesheet')}`);
27
35
  if (this.dryRun) {
28
36
  await this.previewRemote(getResourceStateProvider('template'), templatesPath);
29
37
  this.log('[dry-run] Would rewrite the child theme with them.');
30
38
  return { ...names, status: 'dry-run' };
31
39
  }
32
- const child = await this.wp.put(CHILD_THEME_ENDPOINT, { parts, templates });
40
+ const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts, templates });
33
41
  await this.recordSuccess();
34
42
  this.log(`Child theme ${child.stylesheet} written.`);
35
43
  if (!child.active) {
@@ -39,6 +47,19 @@ export default class Push extends PushCommand {
39
47
  this.warn(`Edited in the Site Editor, their database copy still wins over the pushed file: ${child.customized.join(', ')}. ` +
40
48
  'Clear their customizations in Appearance > Editor to use the pushed version.');
41
49
  }
42
- return { ...names, active: child.active, customized: child.customized, status: 'success', stylesheet: child.stylesheet };
50
+ // A warning, never a refusal: the block may come from a plugin active on another environment.
51
+ const unknownBlocks = child.unknownBlocks ?? [];
52
+ if (unknownBlocks.length > 0) {
53
+ this.warn(`No block registered on this site under these names, their CSS will never load: ${unknownBlocks.join(', ')}. ` +
54
+ 'Check the file name in theme/blocks/, or activate the plugin that provides the block.');
55
+ }
56
+ return {
57
+ ...names,
58
+ active: child.active,
59
+ customized: child.customized,
60
+ status: 'success',
61
+ stylesheet: child.stylesheet,
62
+ unknownBlocks,
63
+ };
43
64
  }
44
65
  }
@@ -16,8 +16,13 @@ export default class Pull extends LoopressCommand {
16
16
  const { url } = this.siteConfig;
17
17
  this.log(`Pulling themes from ${url}`);
18
18
  const raw = await this.wp.get('wp/v2/themes');
19
+ // A theme with no Version header can't be pinned, and `theme version push` would then fail on
20
+ // it: the Loopress child theme (`<parent>-loopress`, written by `theme template push`) is one.
19
21
  const installed = parseInstalledThemes(raw);
20
- const incoming = Object.fromEntries(installed.map((t) => [t.slug, t.version]));
22
+ const unversioned = installed.filter((t) => t.version === '').map((t) => t.slug);
23
+ const incoming = Object.fromEntries(installed.filter((t) => t.version !== '').map((t) => [t.slug, t.version]));
24
+ if (unversioned.length > 0)
25
+ this.log(` - Skipped: ${unversioned.join(', ')} (no version to pin)`);
21
26
  // A composer.json is authoritative over loopress.json: pin the live versions there instead.
22
27
  const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
23
28
  if (existsSync(composerJsonPath)) {
@@ -8,15 +8,15 @@ export declare class ProjectConfigManager {
8
8
  id: string;
9
9
  });
10
10
  getConfigFilePath(): string;
11
- getCurrentEnv(): EnvironmentConfig | null;
12
11
  getCurrentProject(): null | (ProjectConfig & {
13
12
  id: string;
14
13
  });
14
+ getDefaultEnvironment(projectId: string): EnvironmentConfig | null;
15
15
  getEnvironment(projectId: string, envName: string): EnvironmentConfig | null;
16
16
  getProject(id: string): null | ProjectConfig;
17
17
  isTelemetryDisabled(): boolean;
18
18
  listEnvironments(projectId: string): Array<EnvironmentConfig & {
19
- isCurrent: boolean;
19
+ isDefault: boolean;
20
20
  }>;
21
21
  listProjects(): Array<ProjectConfig & {
22
22
  id: string;
@@ -26,7 +26,7 @@ export declare class ProjectConfigManager {
26
26
  removeEnvironment(projectId: string, envName: string): void;
27
27
  removeProject(id: string): void;
28
28
  setConfigDir(configDir: string): void;
29
- setCurrent(projectId: string, envName: string): void;
29
+ setCurrent(projectId: string): void;
30
30
  setEnvironment(projectId: string, envName: string, env: EnvironmentConfig): void;
31
31
  setEnvironmentApiId(projectId: string, envName: string, apiEnvironmentId: string): void;
32
32
  setProject(id: string, project: ProjectConfig): void;