@loopress/cli 0.26.0 → 0.28.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 (87) hide show
  1. package/README.md +275 -177
  2. package/dist/commands/app/pull.js +17 -4
  3. package/dist/commands/app/push.d.ts +3 -0
  4. package/dist/commands/app/push.js +29 -5
  5. package/dist/commands/composer/push.js +1 -19
  6. package/dist/commands/diff.js +1 -1
  7. package/dist/commands/init.js +7 -1
  8. package/dist/commands/page/push.js +2 -2
  9. package/dist/commands/plugin/add.d.ts +8 -1
  10. package/dist/commands/plugin/add.js +10 -5
  11. package/dist/commands/plugin/audit.js +4 -5
  12. package/dist/commands/plugin/pull.d.ts +3 -1
  13. package/dist/commands/plugin/pull.js +17 -9
  14. package/dist/commands/plugin/push.d.ts +5 -1
  15. package/dist/commands/plugin/push.js +90 -37
  16. package/dist/commands/plugin/status.d.ts +1 -0
  17. package/dist/commands/plugin/status.js +21 -2
  18. package/dist/commands/project/config.js +12 -8
  19. package/dist/commands/promote.d.ts +2 -0
  20. package/dist/commands/promote.js +70 -8
  21. package/dist/commands/pull.js +8 -4
  22. package/dist/commands/push.d.ts +1 -0
  23. package/dist/commands/push.js +42 -4
  24. package/dist/commands/template/diff.d.ts +10 -0
  25. package/dist/commands/template/diff.js +13 -0
  26. package/dist/commands/template/list.d.ts +13 -0
  27. package/dist/commands/template/list.js +23 -0
  28. package/dist/commands/template/push.d.ts +20 -0
  29. package/dist/commands/template/push.js +42 -0
  30. package/dist/commands/theme/add.d.ts +8 -1
  31. package/dist/commands/theme/add.js +4 -2
  32. package/dist/commands/theme/pull.d.ts +3 -1
  33. package/dist/commands/theme/pull.js +10 -4
  34. package/dist/commands/theme/style/diff.d.ts +2 -0
  35. package/dist/commands/{theme-styles → theme/style}/diff.js +2 -1
  36. package/dist/commands/{theme-styles → theme/style}/pull.d.ts +1 -1
  37. package/dist/commands/{theme-styles → theme/style}/pull.js +3 -3
  38. package/dist/commands/{theme-styles → theme/style}/push.d.ts +1 -1
  39. package/dist/commands/{theme-styles → theme/style}/push.js +5 -5
  40. package/dist/commands/theme/style/rollback.d.ts +2 -0
  41. package/dist/commands/theme/style/rollback.js +6 -0
  42. package/dist/lib/api-client.js +26 -1
  43. package/dist/lib/app-build.d.ts +5 -0
  44. package/dist/lib/app-build.js +69 -0
  45. package/dist/lib/app-manifest.d.ts +4 -0
  46. package/dist/lib/app-manifest.js +8 -0
  47. package/dist/lib/base.js +11 -2
  48. package/dist/lib/bootstrap-full-install.js +45 -14
  49. package/dist/lib/dev-watch.d.ts +1 -1
  50. package/dist/lib/dev-watch.js +4 -1
  51. package/dist/lib/diff-command.d.ts +1 -0
  52. package/dist/lib/diff-command.js +6 -4
  53. package/dist/lib/json-delegation.d.ts +1 -0
  54. package/dist/lib/json-delegation.js +18 -0
  55. package/dist/lib/load-snippets.js +1 -1
  56. package/dist/lib/php-files-command.js +4 -3
  57. package/dist/lib/readdir-tolerant.js +15 -5
  58. package/dist/lib/resource-state.js +43 -1
  59. package/dist/lib/rollback-command.d.ts +1 -0
  60. package/dist/lib/rollback-command.js +8 -6
  61. package/dist/lib/rotate-app-password.js +2 -2
  62. package/dist/lib/snapshot-store.js +14 -0
  63. package/dist/lib/validate-local.js +34 -7
  64. package/dist/lib/wp-client.d.ts +4 -1
  65. package/dist/lib/wp-client.js +56 -3
  66. package/dist/types/plugin.d.ts +8 -1
  67. package/dist/types/plugin.js +4 -1
  68. package/dist/types/project-config.generated.d.ts +19 -2
  69. package/dist/utils/composer.d.ts +14 -0
  70. package/dist/utils/composer.js +136 -0
  71. package/dist/utils/loopress-config.d.ts +1 -0
  72. package/dist/utils/loopress-config.js +14 -0
  73. package/dist/utils/page-format.d.ts +12 -0
  74. package/dist/utils/page-format.js +41 -26
  75. package/dist/utils/plugins.d.ts +9 -4
  76. package/dist/utils/plugins.js +16 -7
  77. package/dist/utils/resource-dirs.d.ts +2 -0
  78. package/dist/utils/resource-dirs.js +4 -0
  79. package/dist/utils/seo-format.d.ts +2 -2
  80. package/dist/utils/template-format.d.ts +30 -0
  81. package/dist/utils/template-format.js +36 -0
  82. package/oclif.manifest.json +536 -320
  83. package/package.json +7 -4
  84. package/schemas/project-config.schema.json +25 -4
  85. package/dist/commands/theme-styles/diff.d.ts +0 -2
  86. package/dist/commands/theme-styles/rollback.d.ts +0 -2
  87. package/dist/commands/theme-styles/rollback.js +0 -5
@@ -2,9 +2,9 @@ import { confirm } from '@inquirer/prompts';
2
2
  import { Args } from '@oclif/core';
3
3
  import { Buffer } from 'node:buffer';
4
4
  import { existsSync } from 'node:fs';
5
- import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
5
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
6
6
  import { dirname, join } from 'node:path';
7
- import { APP_CONFIG_FILENAME } from '../../lib/app-manifest.js';
7
+ import { APP_CONFIG_FILENAME, parseAppConfig } from '../../lib/app-manifest.js';
8
8
  import { LoopressCommand } from '../../lib/base.js';
9
9
  import { isInteractive } from '../../lib/interactive.js';
10
10
  import { pluralize } from '../../utils/pluralize.js';
@@ -60,14 +60,27 @@ export default class Pull extends LoopressCommand {
60
60
  }
61
61
  async pullApp(path, name) {
62
62
  const manifest = await this.wp.get(`loopress/v1/apps/${name}/manifest`);
63
- const distDir = join(path, name, 'dist');
63
+ const appDir = join(path, name);
64
+ const configPath = join(appDir, APP_CONFIG_FILENAME);
65
+ // WordPress doesn't know where the app builds to (assetsDir) or whether its entry is explicit:
66
+ // keep what the local config says, only refresh what the server is the source of truth for.
67
+ const local = existsSync(configPath) ? parseAppConfig(await readFile(configPath, 'utf8'), appDir) : {};
68
+ const assetsDir = local.assetsDir ?? 'dist';
69
+ const distDir = join(appDir, assetsDir);
64
70
  for (const file of manifest.files) {
65
71
  const { content } = await this.wp.get(`loopress/v1/apps/${name}/asset?path=${encodeURIComponent(file.path)}`);
66
72
  const target = join(distDir, file.path);
67
73
  await mkdir(dirname(target), { recursive: true });
68
74
  await writeFile(target, Buffer.from(content, 'base64'));
69
75
  }
70
- await writeFile(join(path, name, APP_CONFIG_FILENAME), JSON.stringify({ assetsDir: 'dist', mountSelector: manifest.mountSelector, name: manifest.name, routing: manifest.routing }, null, 2) + '\n');
76
+ await writeFile(configPath, JSON.stringify({
77
+ ...local,
78
+ assetsDir,
79
+ crossOrigin: manifest.crossOrigin ?? undefined, // dropped by JSON.stringify when unset
80
+ mountSelector: manifest.mountSelector,
81
+ name: manifest.name,
82
+ routing: manifest.routing,
83
+ }, null, 2) + '\n');
71
84
  this.log(` ${name}: ${pluralize(manifest.files.length, 'file')} (build ${manifest.buildId})`);
72
85
  return { files: manifest.files.length, name };
73
86
  }
@@ -18,11 +18,14 @@ export default class Push extends PushCommand {
18
18
  static flags: {
19
19
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
+ build: import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
22
  };
23
+ private build;
22
24
  run(): Promise<PushResult>;
23
25
  private appDirNames;
24
26
  private pushApp;
25
27
  private remoteFiles;
26
28
  private uploadAsset;
29
+ private warnIfCrossOriginIgnored;
27
30
  }
28
31
  export {};
@@ -1,7 +1,8 @@
1
- import { Args } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { readdir, readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { buildApp } from '../../lib/app-build.js';
5
6
  import { APP_CONFIG_FILENAME, diffFiles, loadAppManifest, } from '../../lib/app-manifest.js';
6
7
  import { PushCommand } from '../../lib/push-command.js';
7
8
  import { isNotFoundError } from '../../lib/wp-client.js';
@@ -10,15 +11,22 @@ export default class Push extends PushCommand {
10
11
  static args = {
11
12
  name: Args.string({ description: 'Push only this app (defaults to every app in the directory)' }),
12
13
  };
13
- static description = 'Push built single-page app bundles to WordPress. Each app is an `apps/<name>/` directory holding a loopress.app.json and a built dist/ folder. Only files whose content changed are uploaded.';
14
+ static description = 'Build and push single-page app bundles to WordPress. Each app is an `apps/<name>/` directory holding a loopress.app.json and a dist/ folder. When the app has a package.json with a `build` script, it runs first (use --no-build to skip it). Only files whose content changed are uploaded.';
14
15
  static enableJsonFlag = true;
15
- static examples = ['$ lps app push', '$ lps app push search'];
16
+ static examples = ['$ lps app push', '$ lps app push search', '$ lps app push --no-build'];
16
17
  static flags = {
18
+ build: Flags.boolean({
19
+ allowNo: true,
20
+ default: true,
21
+ description: "Run the app's package.json build script before pushing",
22
+ }),
17
23
  ...PushCommand.dryRunFlag,
18
24
  ...PushCommand.yesFlag,
19
25
  };
26
+ build = true;
20
27
  async run() {
21
- const { args } = await this.parse(Push);
28
+ const { args, flags } = await this.parse(Push);
29
+ this.build = flags.build;
22
30
  const { url } = this.siteConfig;
23
31
  const path = this.resolveAppsPath();
24
32
  this.log(`Pushing apps to ${url}`);
@@ -71,6 +79,13 @@ export default class Push extends PushCommand {
71
79
  let manifest;
72
80
  let distDir;
73
81
  try {
82
+ // Built on dry runs too: the preview diffs dist/, so a stale one would make it lie.
83
+ if (this.build) {
84
+ await buildApp(appDir, (command) => {
85
+ if (task)
86
+ task.output = `${dirName}: building (${command})`;
87
+ });
88
+ }
74
89
  ;
75
90
  ({ distDir, manifest } = await loadAppManifest(appDir, dirName));
76
91
  }
@@ -93,12 +108,14 @@ export default class Push extends PushCommand {
93
108
  task.output = `${manifest.name}: uploading ${file.path} (${index + 1}/${changed.length})`;
94
109
  await this.uploadAsset(manifest.name, distDir, file);
95
110
  }
111
+ let committed;
96
112
  try {
97
- await this.wp.post(`loopress/v1/apps/${manifest.name}/commit`, manifest);
113
+ committed = await this.wp.post(`loopress/v1/apps/${manifest.name}/commit`, manifest);
98
114
  }
99
115
  catch (error) {
100
116
  this.reportTaskFailure(`${manifest.name}: commit failed: ${error.message}`, error, task);
101
117
  }
118
+ this.warnIfCrossOriginIgnored(manifest, committed);
102
119
  if (task) {
103
120
  task.output =
104
121
  changed.length === 0
@@ -127,4 +144,11 @@ export default class Push extends PushCommand {
127
144
  this.reportTaskFailure(`${name}: failed to upload ${file.path}: ${error.message}`, error);
128
145
  }
129
146
  }
147
+ // An older plugin, or an app .htaccess the site owner manages, commits the build without the
148
+ // policy: say so rather than let the app look protected when it is not.
149
+ warnIfCrossOriginIgnored(manifest, committed) {
150
+ if (manifest.crossOrigin && committed?.crossOrigin !== manifest.crossOrigin) {
151
+ this.warn(`${manifest.name}: crossOrigin "${manifest.crossOrigin}" was not applied: either the Loopress plugin on this site is too old (update it, then push again) or wp-content/loopress/apps/${manifest.name}/.htaccess is managed by hand and was left untouched.`);
152
+ }
153
+ }
130
154
  }
@@ -4,26 +4,8 @@ import { readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { PushCommand } from '../../lib/push-command.js';
6
6
  import { isTimeoutError } from '../../lib/wp-client.js';
7
+ import { toIntent } from '../../utils/composer.js';
7
8
  import { parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
8
- // Split a composer.json `require` map into the three intent namespaces the sync endpoint
9
- // understands. `composer/installers` is owned by the server scaffold, never sent.
10
- function toIntent(require) {
11
- const intent = { libraries: {}, plugins: {}, themes: {} };
12
- for (const [name, constraint] of Object.entries(require)) {
13
- if (name === 'composer/installers')
14
- continue;
15
- if (name.startsWith('wpackagist-plugin/')) {
16
- intent.plugins[name.slice('wpackagist-plugin/'.length)] = constraint;
17
- }
18
- else if (name.startsWith('wpackagist-theme/')) {
19
- intent.themes[name.slice('wpackagist-theme/'.length)] = constraint;
20
- }
21
- else {
22
- intent.libraries[name] = constraint;
23
- }
24
- }
25
- return intent;
26
- }
27
9
  export default class ComposerPush extends PushCommand {
28
10
  static description = 'Push composer.json to WordPress and run Composer to resolve and install dependencies';
29
11
  static enableJsonFlag = true;
@@ -7,7 +7,7 @@ const RESOURCES = [...RESOURCE_STATE_PROVIDERS.map((provider) => provider.resour
7
7
  export default class Diff extends DiffCommand {
8
8
  static description = [
9
9
  'Show what differs between your local tracked files and a WordPress environment, or between two environments.',
10
- 'Covers snippets, forms, ACF, API routes, Hooks, pages, SEO, Options, Menus, Theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
10
+ 'Covers snippets, forms, ACF, API routes, Hooks, templates, template parts, pages, SEO, Options, Menus, Theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
11
11
  '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
12
  ].join(' ');
13
13
  static enableJsonFlag = true;
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { configManager } from '../config/project-config.manager.js';
6
6
  import { isInteractive } from '../lib/interactive.js';
7
- import { writeLocalConfig } from '../utils/loopress-config.js';
7
+ import { ensureLfGitattributes, writeLocalConfig } from '../utils/loopress-config.js';
8
8
  // WordPress.org slugs for the two snippet plugins the Loopress WordPress plugin supports
9
9
  // (see SnippetModule.php, which wires up both providers and auto-detects the active one).
10
10
  const SNIPPET_PROVIDERS = [
@@ -21,6 +21,8 @@ const FEATURES = [
21
21
  { dir: 'hooks', key: 'hooksDir', label: 'Hooks (actions, filters, cron)' },
22
22
  { dir: 'apps', key: 'appsDir', label: 'Single-page apps' },
23
23
  { dir: 'pages', key: 'pageDir', label: 'Static pages' },
24
+ { dir: 'templates', key: 'templateDir', label: 'Block templates' },
25
+ { dir: 'parts', key: 'partDir', label: 'Block template parts' },
24
26
  ];
25
27
  export default class Init extends Command {
26
28
  static description = 'Initialize a loopress.json config file in the current directory';
@@ -68,6 +70,7 @@ export default class Init extends Command {
68
70
  config[feature.key] = feature.dir;
69
71
  }
70
72
  await writeLocalConfig(config);
73
+ const isGitattributesUpdated = await ensureLfGitattributes();
71
74
  let isProviderAdded = false;
72
75
  if (providerChoice !== '__none__') {
73
76
  try {
@@ -89,6 +92,9 @@ export default class Init extends Command {
89
92
  if (isProviderAdded) {
90
93
  this.log(` Plugin: ${providerChoice}`);
91
94
  }
95
+ if (isGitattributesUpdated) {
96
+ this.log('✓ .gitattributes: LF line endings, so files match across macOS, Linux and Windows');
97
+ }
92
98
  this.log('\n→ Next: run `lps snippet pull` to fetch what is already on the site, or `lps doctor` to verify the connection.');
93
99
  }
94
100
  // When nothing is configured yet, the useful path is configuring a project right here, not
@@ -1,6 +1,6 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { PushCommand } from '../../lib/push-command.js';
3
- import { formatPageProblems, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
3
+ import { formatPageProblems, frontPageNote, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
4
4
  import { pluralize } from '../../utils/pluralize.js';
5
5
  import { resolveResourceDir } from '../../utils/resource-dirs.js';
6
6
  export default class Push extends PushCommand {
@@ -51,7 +51,7 @@ export default class Push extends PushCommand {
51
51
  try {
52
52
  const result = await this.wp.put(PAGES_ENDPOINT, page);
53
53
  if (task)
54
- task.output = `Pushed: ${page.slug} (${result.status}) ${result.link}`;
54
+ task.output = `Pushed: ${page.slug} (${result.status}) ${result.link}${frontPageNote(result)}`;
55
55
  }
56
56
  catch (error) {
57
57
  this.reportTaskFailure(`Failed to push ${page.slug}: ${error.message}`, error, task);
@@ -1,13 +1,20 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
+ type AddResult = {
3
+ slug: string;
4
+ status: 'added' | 'dry-run' | 'unchanged' | 'updated';
5
+ version: string;
6
+ };
2
7
  export default class Add extends LoopressCommand {
3
8
  static args: {
4
9
  slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
10
  };
6
11
  static description: string;
12
+ static enableJsonFlag: boolean;
7
13
  static examples: string[];
8
14
  static flags: {
9
15
  version: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
16
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
17
  };
12
- run(): Promise<void>;
18
+ run(): Promise<AddResult>;
13
19
  }
20
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { LoopressCommand } from '../../lib/base.js';
3
+ import { pinVersion } from '../../types/plugin.js';
3
4
  import { writeLocalConfig } from '../../utils/loopress-config.js';
4
5
  import { isExactVersion } from '../../utils/version.js';
5
6
  export default class Add extends LoopressCommand {
@@ -7,6 +8,7 @@ export default class Add extends LoopressCommand {
7
8
  slug: Args.string({ description: 'Plugin slug on WordPress.org', required: true }),
8
9
  };
9
10
  static description = 'Add a WordPress.org plugin to loopress.json';
11
+ static enableJsonFlag = true;
10
12
  static examples = [
11
13
  '$ lps plugin add woocommerce',
12
14
  '$ lps plugin add woocommerce --version 9.4.2',
@@ -24,19 +26,22 @@ export default class Add extends LoopressCommand {
24
26
  this.error(`--version must be an exact version like 9.4.2, not a Composer constraint. Got "${version}".`);
25
27
  }
26
28
  const existing = this.localConfig.plugins ?? {};
27
- if (existing[slug] === version) {
29
+ const current = existing[slug];
30
+ if (current !== undefined && pinVersion(current) === version) {
28
31
  this.log(`${slug} is already pinned to ${version} in loopress.json, nothing to do.`);
29
- return;
32
+ return { slug, status: 'unchanged', version };
30
33
  }
31
- const isUpdated = existing[slug] !== undefined;
34
+ const isUpdated = current !== undefined;
32
35
  if (this.dryRun) {
33
36
  this.log(`[dry-run] Would ${isUpdated ? 'update' : 'add'} ${slug} (${version}) in loopress.json`);
34
- return;
37
+ return { slug, status: 'dry-run', version };
35
38
  }
36
39
  await writeLocalConfig({
37
40
  ...this.localConfig,
38
- plugins: { ...existing, [slug]: version },
41
+ // Re-pinning a plugin recorded as inactive keeps it inactive.
42
+ plugins: { ...existing, [slug]: typeof current === 'object' ? { ...current, version } : version },
39
43
  });
40
44
  this.log(`${isUpdated ? 'Updated' : 'Added'} ${slug} (${version}). Run \`lps plugin push\` to apply.`);
45
+ return { slug, status: isUpdated ? 'updated' : 'added', version };
41
46
  }
42
47
  }
@@ -1,5 +1,6 @@
1
1
  import got from 'got';
2
2
  import { LoopressCommand } from '../../lib/base.js';
3
+ import { pinVersion } from '../../types/plugin.js';
3
4
  import { compareVersions } from '../../utils/version.js';
4
5
  const WPVULN_BASE = 'https://www.wpvulnerability.net/plugin';
5
6
  const WPORG_INFO = 'https://api.wordpress.org/plugins/info/1.2/';
@@ -18,7 +19,7 @@ export default class Audit extends LoopressCommand {
18
19
  const advisories = [];
19
20
  const health = [];
20
21
  for (const slug of slugs) {
21
- const found = await this.auditOne(slug, manifest[slug]);
22
+ const found = await this.auditOne(slug, pinVersion(manifest[slug]));
22
23
  advisories.push(...found.advisories);
23
24
  health.push(...found.health);
24
25
  }
@@ -26,16 +27,14 @@ export default class Audit extends LoopressCommand {
26
27
  }
27
28
  async auditOne(slug, pinned) {
28
29
  const [vulns, info] = await Promise.all([this.fetchVulnerabilities(slug), this.fetchHealth(slug)]);
29
- const advisories = vulns
30
- .filter((v) => pinned === 'latest' || affects(pinned, v.fixedIn))
31
- .map((v) => ({ ...v, slug }));
30
+ const advisories = vulns.filter((v) => pinned === 'latest' || affects(pinned, v.fixedIn)).map((v) => ({ ...v, slug }));
32
31
  const health = info ? describeHealth(slug, pinned, info) : [];
33
32
  return { advisories, health };
34
33
  }
35
34
  async fetchHealth(slug) {
36
35
  try {
37
36
  const body = await got(WPORG_INFO, {
38
- searchParams: { 'action': 'plugin_information', 'request[slug]': slug },
37
+ searchParams: { action: 'plugin_information', 'request[slug]': slug },
39
38
  timeout: { request: 10_000 },
40
39
  }).json();
41
40
  return typeof body === 'object' && !('error' in body) ? body : null;
@@ -1,7 +1,9 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
+ import { type ComposerPullResult } from '../../utils/composer.js';
2
3
  import { type MergeResult } from '../../utils/plugins.js';
3
4
  type PullResult = MergeResult & {
4
- status: 'composer-managed' | 'dry-run' | 'success';
5
+ skipped?: ComposerPullResult['skipped'];
6
+ status: 'dry-run' | 'success';
5
7
  };
6
8
  export default class Pull extends LoopressCommand {
7
9
  static description: string;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { LoopressCommand } from '../../lib/base.js';
4
+ import { pullIntoComposerJson } from '../../utils/composer.js';
4
5
  import { writeLocalConfig } from '../../utils/loopress-config.js';
5
6
  import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
6
7
  export default class Pull extends LoopressCommand {
@@ -12,18 +13,25 @@ export default class Pull extends LoopressCommand {
12
13
  };
13
14
  async run() {
14
15
  const { url } = this.siteConfig;
15
- if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
16
- this.warn('This project has a composer.json, which is authoritative for plugins. Run `lps composer pull` instead.');
17
- return { added: [], merged: this.localConfig.plugins ?? {}, status: 'composer-managed', updated: [] };
18
- }
19
16
  this.log(`Pulling plugins from ${url}`);
20
17
  const raw = await this.wp.get('wp/v2/plugins');
21
18
  const installed = parseInstalledPlugins(raw);
22
- // Pin every plugin to the version actually running on the site. A later `plugin push`
23
- // installs exactly this set via Composer + WPackagist; drift only surfaces when the pinned
24
- // version and the live version disagree.
25
- const incoming = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
26
- const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, incoming);
19
+ // Pin every plugin to the version actually running on the site, and record the inactive ones
20
+ // as such so a later `plugin push` doesn't switch them on. A later `plugin push` installs
21
+ // exactly this set via Composer + WPackagist; drift only surfaces when the pinned version and
22
+ // the live version disagree.
23
+ const versions = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
24
+ const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
25
+ // A composer.json is authoritative over loopress.json: pin the live versions there instead.
26
+ const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
27
+ if (existsSync(composerJsonPath)) {
28
+ const result = await pullIntoComposerJson(composerJsonPath, 'plugin', versions, {
29
+ dryRun: this.dryRun,
30
+ log: this.log.bind(this),
31
+ });
32
+ return { ...result, status: this.dryRun ? 'dry-run' : 'success' };
33
+ }
34
+ const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, pins);
27
35
  if (this.dryRun) {
28
36
  this.log(`[dry-run] Would write ${Object.keys(merged).length} plugins to loopress.json`);
29
37
  if (added.length > 0)
@@ -1,17 +1,19 @@
1
1
  import { PushCommand } from '../../lib/push-command.js';
2
2
  type PushResult = {
3
3
  activated: string[];
4
+ deactivated: string[];
4
5
  installed: string[];
5
6
  pinned: string[];
6
7
  pruned: string[];
7
8
  removed: string[];
8
- status: 'composer-managed' | 'dry-run' | 'in-sync' | 'success';
9
+ status: 'dry-run' | 'in-sync' | 'success';
9
10
  };
10
11
  export default class Push extends PushCommand {
11
12
  static description: string;
12
13
  static enableJsonFlag: boolean;
13
14
  static examples: string[];
14
15
  static flags: {
16
+ activate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
17
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
18
  prune: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
19
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -24,6 +26,8 @@ export default class Push extends PushCommand {
24
26
  private fetchInstanceLock;
25
27
  private filesForNewlyInstalled;
26
28
  private guardForce;
29
+ private loadSource;
30
+ private logStillInactive;
27
31
  private result;
28
32
  private sync;
29
33
  }