@loopress/cli 0.20.1 → 0.22.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 (118) hide show
  1. package/README.md +254 -75
  2. package/dist/commands/acf/list.d.ts +2 -2
  3. package/dist/commands/acf/list.js +4 -6
  4. package/dist/commands/acf/pull.js +9 -22
  5. package/dist/commands/acf/push.js +4 -7
  6. package/dist/commands/api/list.d.ts +7 -4
  7. package/dist/commands/api/list.js +5 -11
  8. package/dist/commands/api/publish.js +7 -23
  9. package/dist/commands/api/pull.d.ts +8 -1
  10. package/dist/commands/api/pull.js +22 -18
  11. package/dist/commands/api/push.d.ts +7 -1
  12. package/dist/commands/api/push.js +50 -23
  13. package/dist/commands/composer/init.js +6 -6
  14. package/dist/commands/composer/pull.d.ts +7 -1
  15. package/dist/commands/composer/pull.js +4 -2
  16. package/dist/commands/composer/push.d.ts +8 -1
  17. package/dist/commands/composer/push.js +3 -1
  18. package/dist/commands/dev.d.ts +13 -0
  19. package/dist/commands/dev.js +97 -0
  20. package/dist/commands/doctor.js +6 -5
  21. package/dist/commands/form/list.d.ts +2 -4
  22. package/dist/commands/form/list.js +3 -10
  23. package/dist/commands/form/pull.js +15 -24
  24. package/dist/commands/form/push.js +17 -42
  25. package/dist/commands/init.js +7 -7
  26. package/dist/commands/login.js +9 -5
  27. package/dist/commands/logout.js +2 -1
  28. package/dist/commands/page/list.d.ts +2 -4
  29. package/dist/commands/page/list.js +3 -10
  30. package/dist/commands/page/pull.d.ts +13 -1
  31. package/dist/commands/page/pull.js +21 -29
  32. package/dist/commands/page/push.d.ts +11 -1
  33. package/dist/commands/page/push.js +31 -49
  34. package/dist/commands/plugin/add.js +3 -3
  35. package/dist/commands/plugin/pull.d.ts +8 -1
  36. package/dist/commands/plugin/pull.js +11 -11
  37. package/dist/commands/plugin/push.d.ts +12 -1
  38. package/dist/commands/plugin/push.js +41 -27
  39. package/dist/commands/project/config.js +12 -9
  40. package/dist/commands/project/list.d.ts +16 -1
  41. package/dist/commands/project/list.js +22 -16
  42. package/dist/commands/project/pull.js +3 -2
  43. package/dist/commands/project/push.d.ts +5 -0
  44. package/dist/commands/project/push.js +84 -63
  45. package/dist/commands/project/switch.js +2 -2
  46. package/dist/commands/promote.d.ts +18 -0
  47. package/dist/commands/promote.js +90 -0
  48. package/dist/commands/pull.d.ts +12 -0
  49. package/dist/commands/pull.js +51 -0
  50. package/dist/commands/push.d.ts +13 -0
  51. package/dist/commands/push.js +57 -0
  52. package/dist/commands/seo/list.d.ts +9 -2
  53. package/dist/commands/seo/list.js +9 -11
  54. package/dist/commands/seo/pull.js +14 -37
  55. package/dist/commands/seo/push.js +24 -48
  56. package/dist/commands/snippet/list.d.ts +3 -4
  57. package/dist/commands/snippet/list.js +5 -11
  58. package/dist/commands/snippet/publish.js +5 -21
  59. package/dist/commands/snippet/pull.d.ts +13 -1
  60. package/dist/commands/snippet/pull.js +18 -20
  61. package/dist/commands/snippet/push.d.ts +11 -1
  62. package/dist/commands/snippet/push.js +29 -28
  63. package/dist/commands/status.d.ts +13 -1
  64. package/dist/commands/status.js +25 -18
  65. package/dist/commands/validate.d.ts +8 -0
  66. package/dist/commands/validate.js +24 -0
  67. package/dist/config/auth.manager.d.ts +2 -2
  68. package/dist/config/project-config.manager.d.ts +1 -1
  69. package/dist/config/project-config.manager.js +5 -6
  70. package/dist/lib/base.d.ts +11 -2
  71. package/dist/lib/base.js +54 -24
  72. package/dist/lib/bootstrap-full-install.d.ts +1 -1
  73. package/dist/lib/dev-watch.d.ts +16 -0
  74. package/dist/lib/dev-watch.js +71 -0
  75. package/dist/lib/find-orphaned-files.d.ts +3 -2
  76. package/dist/lib/find-orphaned-files.js +40 -4
  77. package/dist/lib/github-release.js +1 -1
  78. package/dist/lib/guard-production-push.d.ts +12 -0
  79. package/dist/lib/guard-production-push.js +20 -0
  80. package/dist/lib/interactive.js +1 -1
  81. package/dist/lib/load-files.d.ts +1 -0
  82. package/dist/lib/load-files.js +40 -11
  83. package/dist/lib/load-snippets.d.ts +1 -1
  84. package/dist/lib/load-snippets.js +66 -59
  85. package/dist/lib/local-callback-server.js +15 -8
  86. package/dist/lib/plugin-detection.d.ts +1 -1
  87. package/dist/lib/push-command.d.ts +3 -0
  88. package/dist/lib/push-command.js +22 -15
  89. package/dist/lib/put-or-create.d.ts +13 -0
  90. package/dist/lib/put-or-create.js +19 -0
  91. package/dist/lib/readdir-tolerant.d.ts +5 -0
  92. package/dist/lib/readdir-tolerant.js +11 -0
  93. package/dist/lib/resolve-linked-project.d.ts +8 -0
  94. package/dist/lib/resolve-linked-project.js +27 -0
  95. package/dist/lib/rotate-app-password.d.ts +1 -1
  96. package/dist/lib/rotate-app-password.js +1 -1
  97. package/dist/lib/temp-admin.d.ts +3 -3
  98. package/dist/lib/temp-admin.js +2 -2
  99. package/dist/lib/validate-local.d.ts +10 -0
  100. package/dist/lib/validate-local.js +183 -0
  101. package/dist/lib/wp-authorize-flow.js +5 -1
  102. package/dist/lib/wp-client.d.ts +2 -2
  103. package/dist/lib/wp-client.js +7 -0
  104. package/dist/types/plugin.d.ts +4 -4
  105. package/dist/types/snippet.d.ts +3 -3
  106. package/dist/utils/composer.d.ts +2 -2
  107. package/dist/utils/form-format.js +1 -1
  108. package/dist/utils/loopress-config.d.ts +1 -1
  109. package/dist/utils/page-format.js +2 -2
  110. package/dist/utils/plugins.d.ts +5 -5
  111. package/dist/utils/plugins.js +3 -3
  112. package/dist/utils/pluralize.d.ts +1 -0
  113. package/dist/utils/pluralize.js +5 -0
  114. package/dist/utils/seo-format.d.ts +6 -6
  115. package/dist/utils/snippet-format.d.ts +2 -2
  116. package/dist/utils/snippet-format.js +10 -5
  117. package/oclif.manifest.json +394 -57
  118. package/package.json +18 -16
@@ -1,15 +1,17 @@
1
1
  import { Args } from '@oclif/core';
2
- import { Listr } from 'listr2';
3
- import { readdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { readFile, rename, writeFile } from 'node:fs/promises';
4
3
  import { basename, dirname, extname, join } from 'node:path';
5
4
  import { PushCommand } from '../../lib/push-command.js';
6
- import { isNotFoundError } from '../../lib/wp-client.js';
5
+ import { putOrCreate } from '../../lib/put-or-create.js';
6
+ import { readdirTolerant } from '../../lib/readdir-tolerant.js';
7
7
  import { getPageId, getPageTitle, PAGE_ENDPOINT, pageFileBase } from '../../utils/page-format.js';
8
+ import { pluralize } from '../../utils/pluralize.js';
8
9
  export default class Push extends PushCommand {
9
10
  static args = {
10
11
  path: Args.string({ description: 'Path to pages directory (overrides project config)' }),
11
12
  };
12
13
  static description = 'Push pages to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
14
+ static enableJsonFlag = true;
13
15
  static examples = ['$ lps page push'];
14
16
  static flags = {
15
17
  ...PushCommand.dryRunFlag,
@@ -22,18 +24,20 @@ export default class Push extends PushCommand {
22
24
  this.log(`Pushing pages to ${url}`);
23
25
  this.log(`Pages path: ${path}`);
24
26
  const pages = await this.loadFiles(path);
25
- this.log(`Found ${pages.length} page${pages.length === 1 ? '' : 's'} to push`);
26
- await new Listr(pages.map((page) => ({
27
- task: async (_ctx, task) => this.pushPage(page, task),
28
- title: `Push ${getPageTitle(page.meta)}`,
29
- })), { concurrent: false, exitOnError: false }).run();
27
+ this.log(`Found ${pluralize(pages.length, 'page')} to push`);
28
+ const pushed = [];
29
+ await this.runPushTasks(pages, (page) => getPageTitle(page.meta), async (page, task) => {
30
+ const id = await this.pushPage(page, task);
31
+ pushed.push({ id, title: getPageTitle(page.meta) });
32
+ });
30
33
  if (this.failedCount > 0) {
31
- this.error(`${this.failedCount} page${this.failedCount === 1 ? '' : 's'} failed to push.`);
34
+ this.error(`${pluralize(this.failedCount, 'page')} failed to push.`);
32
35
  }
33
36
  if (this.dryRun)
34
- return;
37
+ return { pushed, status: 'dry-run' };
35
38
  await this.recordSuccess();
36
39
  this.log('All pages pushed.');
40
+ return { pushed, status: 'success' };
37
41
  }
38
42
  // Renames the local file pair to the `<id>-<slug>` convention used by `page pull` whenever it
39
43
  // doesn't already match, same principle as ensureCanonicalFilename in commands/snippet/push.ts:
@@ -55,15 +59,7 @@ export default class Push extends PushCommand {
55
59
  // corrupted or hand-broken sidecar must only skip that page, not abort loading the rest of
56
60
  // the directory, same principle as commands/form/push.ts.
57
61
  async loadFiles(dir) {
58
- let files;
59
- try {
60
- files = await readdir(dir);
61
- }
62
- catch (error) {
63
- if (error.code === 'ENOENT')
64
- return [];
65
- throw error;
66
- }
62
+ const files = await readdirTolerant(dir);
67
63
  const pages = [];
68
64
  for (const file of files) {
69
65
  if (extname(file) !== '.html')
@@ -97,8 +93,6 @@ export default class Push extends PushCommand {
97
93
  }
98
94
  return pages;
99
95
  }
100
- // PUT-then-404-fallback-POST, same dance as commands/form/push.ts: pages are id-based.
101
- //
102
96
  // ponytail: `parent` round-trips as the source site's numeric page id untouched, so pushing
103
97
  // to a site where that id belongs to a different page (or nothing) silently mis-parents or
104
98
  // orphans the page. No id-remapping across sites here; fine for same-hierarchy environments,
@@ -108,40 +102,28 @@ export default class Push extends PushCommand {
108
102
  if (this.dryRun) {
109
103
  if (task)
110
104
  task.output = `[dry-run] Would push: ${title}`;
111
- return;
105
+ return getPageId(page.meta);
112
106
  }
113
107
  try {
114
108
  const id = getPageId(page.meta);
115
109
  const payload = { ...page.meta, content: page.content };
116
- if (id === null) {
117
- const created = await this.wp.post(PAGE_ENDPOINT, payload);
118
- const newId = getPageId(created);
119
- if (newId !== null)
120
- await this.ensureCanonicalFilename(page, newId, title);
121
- }
122
- else {
123
- try {
124
- await this.wp.put(`${PAGE_ENDPOINT}/${id}`, payload);
125
- await this.ensureCanonicalFilename(page, id, title);
126
- }
127
- catch (error) {
128
- // The id recorded locally doesn't exist on this site (e.g. a fresh install): create
129
- // it instead of failing, and adopt whatever id the site assigns. WordPress core
130
- // rejects any POST that still carries a (now stale) `id` field with a 400 "Cannot
131
- // create existing post", regardless of whether that id actually exists, so it must
132
- // be stripped before falling back to create.
133
- if (!isNotFoundError(error))
134
- throw error;
135
- const createPayload = { ...payload };
136
- delete createPayload.id;
137
- const created = await this.wp.post(PAGE_ENDPOINT, createPayload);
138
- const newId = getPageId(created);
139
- if (newId !== null)
140
- await this.ensureCanonicalFilename(page, newId, title);
141
- }
142
- }
110
+ // WordPress core rejects any POST that still carries a (now stale) `id` field with a
111
+ // 400 "Cannot create existing post", regardless of whether that id actually exists.
112
+ const postPayload = { ...payload };
113
+ delete postPayload.id;
114
+ const { body, created } = await putOrCreate(this.wp, {
115
+ id,
116
+ payload,
117
+ postEndpoint: PAGE_ENDPOINT,
118
+ postPayload,
119
+ putEndpoint: (pageId) => `${PAGE_ENDPOINT}/${pageId}`,
120
+ });
121
+ const resultId = created ? getPageId(body) : id;
122
+ if (resultId !== null)
123
+ await this.ensureCanonicalFilename(page, resultId, title);
143
124
  if (task)
144
125
  task.output = `Pushed: ${title}`;
126
+ return resultId;
145
127
  }
146
128
  catch (error) {
147
129
  this.reportTaskFailure(`Failed to push ${title}: ${error.message}`, error, task);
@@ -18,15 +18,15 @@ export default class Add extends LoopressCommand {
18
18
  this.log(`${slug} is already in loopress.json, nothing to do.`);
19
19
  return;
20
20
  }
21
- const updated = existing[slug] !== undefined;
21
+ const isUpdated = existing[slug] !== undefined;
22
22
  if (this.dryRun) {
23
- this.log(`[dry-run] Would ${updated ? 'update' : 'add'} ${slug} in loopress.json`);
23
+ this.log(`[dry-run] Would ${isUpdated ? 'update' : 'add'} ${slug} in loopress.json`);
24
24
  return;
25
25
  }
26
26
  await writeLocalConfig({
27
27
  ...this.localConfig,
28
28
  plugins: { ...existing, [slug]: 'latest' },
29
29
  });
30
- this.log(`${updated ? 'Updated' : 'Added'} ${slug}`);
30
+ this.log(`${isUpdated ? 'Updated' : 'Added'} ${slug}`);
31
31
  }
32
32
  }
@@ -1,9 +1,16 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
+ import { type MergeResult } from '../../utils/plugins.js';
3
+ type PullResult = MergeResult & {
4
+ status: 'dry-run' | 'success';
5
+ };
2
6
  export default class Pull extends LoopressCommand {
3
7
  static description: string;
8
+ static enableJsonFlag: boolean;
4
9
  static examples: string[];
5
10
  static flags: {
6
11
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
12
  };
8
- run(): Promise<void>;
13
+ run(): Promise<PullResult>;
14
+ private logSkippedComposerManaged;
9
15
  }
16
+ export {};
@@ -4,6 +4,7 @@ import { writeLocalConfig } from '../../utils/loopress-config.js';
4
4
  import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
5
5
  export default class Pull extends LoopressCommand {
6
6
  static description = 'Pull installed plugins from WordPress into loopress.json';
7
+ static enableJsonFlag = true;
7
8
  static examples = ['$ lps plugin pull', '$ lps plugin pull --dry-run'];
8
9
  static flags = {
9
10
  ...LoopressCommand.dryRunFlag,
@@ -16,22 +17,15 @@ export default class Pull extends LoopressCommand {
16
17
  const composerJson = await readComposerJson();
17
18
  const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
18
19
  const incoming = Object.fromEntries(installed.filter((p) => !composerSlugs.includes(p.slug)).map((p) => [p.slug, 'latest']));
19
- if (composerSlugs.length > 0) {
20
- const found = installed.filter((p) => composerSlugs.includes(p.slug)).map((p) => p.slug);
21
- if (found.length > 0) {
22
- this.log(`Skipping ${found.length} Composer-managed ${found.length === 1 ? 'plugin' : 'plugins'}: ${found.join(', ')}`);
23
- }
24
- }
20
+ this.logSkippedComposerManaged(installed.filter((p) => composerSlugs.includes(p.slug)).map((p) => p.slug));
25
21
  const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, incoming);
26
22
  if (this.dryRun) {
27
23
  this.log(`[dry-run] Would write ${Object.keys(merged).length} plugins to loopress.json`);
28
24
  if (added.length > 0)
29
25
  this.log(` + ${added.join(', ')}`);
30
- if (updated.length > 0) {
31
- for (const u of updated)
32
- this.log(` ~ ${u.slug} (${u.from} → ${u.to})`);
33
- }
34
- return;
26
+ for (const u of updated)
27
+ this.log(` ~ ${u.slug} (${u.from} → ${u.to})`);
28
+ return { added, merged, status: 'dry-run', updated };
35
29
  }
36
30
  await writeLocalConfig({ ...this.localConfig, plugins: merged });
37
31
  this.log(`Wrote ${Object.keys(merged).length} plugins to loopress.json`);
@@ -39,5 +33,11 @@ export default class Pull extends LoopressCommand {
39
33
  this.log(` + Added: ${added.join(', ')}`);
40
34
  for (const u of updated)
41
35
  this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
36
+ return { added, merged, status: 'success', updated };
37
+ }
38
+ logSkippedComposerManaged(found) {
39
+ if (found.length > 0) {
40
+ this.log(`Skipping ${found.length} Composer-managed ${found.length === 1 ? 'plugin' : 'plugins'}: ${found.join(', ')}`);
41
+ }
42
42
  }
43
43
  }
@@ -1,13 +1,24 @@
1
1
  import { PushCommand } from '../../lib/push-command.js';
2
+ type PushResult = {
3
+ activated: string[];
4
+ installed: string[];
5
+ skippedComposerManaged: string[];
6
+ status: 'dry-run' | 'in-sync' | 'success';
7
+ };
2
8
  export default class Push extends PushCommand {
3
9
  static description: string;
10
+ static enableJsonFlag: boolean;
4
11
  static examples: string[];
5
12
  static flags: {
6
13
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
14
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
15
  };
9
- run(): Promise<void>;
16
+ run(): Promise<PushResult>;
10
17
  private activatePlugin;
18
+ private applyPluginChanges;
11
19
  private installPlugin;
20
+ private logPlannedChanges;
21
+ private logSkippedComposerManaged;
12
22
  private performPluginAction;
13
23
  }
24
+ export {};
@@ -2,8 +2,10 @@ import { Listr } from 'listr2';
2
2
  import { PushCommand } from '../../lib/push-command.js';
3
3
  import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
4
4
  import { diffPlugins, parseInstalledPlugins } from '../../utils/plugins.js';
5
+ import { pluralize } from '../../utils/pluralize.js';
5
6
  export default class Push extends PushCommand {
6
7
  static description = 'Push plugins to WordPress to match loopress.json';
8
+ static enableJsonFlag = true;
7
9
  static examples = ['$ lps plugin push', '$ lps plugin push --dry-run'];
8
10
  static flags = {
9
11
  ...PushCommand.dryRunFlag,
@@ -18,55 +20,67 @@ export default class Push extends PushCommand {
18
20
  const composerJson = await readComposerJson();
19
21
  const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
20
22
  const filteredManifest = Object.fromEntries(Object.entries(manifest).filter(([slug]) => !composerSlugs.includes(slug)));
21
- const skipped = composerSlugs.filter((slug) => slug in manifest);
22
- if (skipped.length > 0) {
23
- this.log(`Skipping ${skipped.length} Composer-managed ${skipped.length === 1 ? 'plugin' : 'plugins'}: ${skipped.join(', ')}`);
24
- this.log('Run `lps composer push` to deploy them.');
25
- }
23
+ const skippedComposerManaged = composerSlugs.filter((slug) => Object.hasOwn(manifest, slug));
24
+ this.logSkippedComposerManaged(skippedComposerManaged);
26
25
  this.log(`Pushing plugins to ${url}`);
27
26
  const raw = await this.wp.get('wp/v2/plugins');
28
27
  const installed = parseInstalledPlugins(raw);
29
28
  const { toActivate, toInstall } = diffPlugins(filteredManifest, installed);
30
29
  if (toInstall.length === 0 && toActivate.length === 0) {
31
30
  this.log('Everything is already in sync.');
32
- return;
33
- }
34
- if (toInstall.length > 0) {
35
- this.log(`\nTo install (${toInstall.length}):`);
36
- for (const a of toInstall)
37
- this.log(` + ${a.slug}`);
38
- }
39
- if (toActivate.length > 0) {
40
- this.log(`\nTo activate (${toActivate.length}):`);
41
- for (const a of toActivate)
42
- this.log(` ↑ ${a.slug}`);
31
+ return { activated: [], installed: [], skippedComposerManaged, status: 'in-sync' };
43
32
  }
33
+ this.logPlannedChanges(toInstall, toActivate);
34
+ const installedSlugs = toInstall.map((a) => a.slug);
35
+ const activatedSlugs = toActivate.map((a) => a.slug);
44
36
  if (this.dryRun)
45
- return;
37
+ return { activated: activatedSlugs, installed: installedSlugs, skippedComposerManaged, status: 'dry-run' };
46
38
  // Installing with `status: active` activates in the same call, so installs never need a
47
39
  // separate activation step the way the old custom endpoint's two-step flow did.
40
+ await this.applyPluginChanges(toInstall, toActivate);
41
+ if (this.failedCount > 0) {
42
+ this.error(`${pluralize(this.failedCount, 'plugin')} failed to install or activate.`);
43
+ }
44
+ await this.recordSuccess();
45
+ return { activated: activatedSlugs, installed: installedSlugs, skippedComposerManaged, status: 'success' };
46
+ }
47
+ async activatePlugin(file, slug, task) {
48
+ await this.performPluginAction('activate', slug, async () => this.wp.put(`wp/v2/plugins/${file}`, { status: 'active' }), task);
49
+ }
50
+ async applyPluginChanges(toInstall, toActivate) {
48
51
  if (toInstall.length > 0) {
49
52
  await new Listr(toInstall.map((action) => ({
50
53
  task: async (_ctx, task) => this.installPlugin(action.slug, task),
51
54
  title: `Install ${action.slug}`,
52
- })), { concurrent: false, exitOnError: false }).run();
55
+ })), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
53
56
  }
54
57
  if (toActivate.length > 0) {
55
58
  await new Listr(toActivate.map((action) => ({
56
59
  task: async (_ctx, task) => this.activatePlugin(action.file, action.slug, task),
57
60
  title: `Activate ${action.slug}`,
58
- })), { concurrent: false, exitOnError: false }).run();
61
+ })), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
59
62
  }
60
- if (this.failedCount > 0) {
61
- this.error(`${this.failedCount} plugin${this.failedCount === 1 ? '' : 's'} failed to install or activate.`);
62
- }
63
- await this.recordSuccess();
64
- }
65
- async activatePlugin(file, slug, task) {
66
- await this.performPluginAction('activate', slug, () => this.wp.put(`wp/v2/plugins/${file}`, { status: 'active' }), task);
67
63
  }
68
64
  async installPlugin(slug, task) {
69
- await this.performPluginAction('install', slug, () => this.wp.post('wp/v2/plugins', { slug, status: 'active' }), task);
65
+ await this.performPluginAction('install', slug, async () => this.wp.post('wp/v2/plugins', { slug, status: 'active' }), task);
66
+ }
67
+ logPlannedChanges(toInstall, toActivate) {
68
+ if (toInstall.length > 0) {
69
+ this.log(`\nTo install (${toInstall.length}):`);
70
+ for (const a of toInstall)
71
+ this.log(` + ${a.slug}`);
72
+ }
73
+ if (toActivate.length > 0) {
74
+ this.log(`\nTo activate (${toActivate.length}):`);
75
+ for (const a of toActivate)
76
+ this.log(` ↑ ${a.slug}`);
77
+ }
78
+ }
79
+ logSkippedComposerManaged(skippedComposerManaged) {
80
+ if (skippedComposerManaged.length === 0)
81
+ return;
82
+ this.log(`Skipping ${skippedComposerManaged.length} Composer-managed ${skippedComposerManaged.length === 1 ? 'plugin' : 'plugins'}: ${skippedComposerManaged.join(', ')}`);
83
+ this.log('Run `lps composer push` to deploy them.');
70
84
  }
71
85
  async performPluginAction(verb, slug, request, task) {
72
86
  try {
@@ -36,11 +36,11 @@ export default class Config extends Command {
36
36
  : envChoice;
37
37
  const existingEnv = configManager.getEnvironment(projectId, envName);
38
38
  if (existingEnv) {
39
- const overwrite = await confirm({
39
+ const isOverwrite = await confirm({
40
40
  default: false,
41
41
  message: `"${projectName}/${envName}" already exists. Overwrite?`,
42
42
  });
43
- if (!overwrite) {
43
+ if (!isOverwrite) {
44
44
  this.log('Aborted.');
45
45
  return;
46
46
  }
@@ -60,7 +60,10 @@ export default class Config extends Command {
60
60
  }
61
61
  },
62
62
  });
63
- const url = rawUrl.replace(/\/+$/, '');
63
+ let url = rawUrl;
64
+ while (url.endsWith('/')) {
65
+ url = url.slice(0, -1);
66
+ }
64
67
  const { appPassword, user } = await this.resolveCredentials(url);
65
68
  const token = `${user}:${appPassword}`;
66
69
  const env = {
@@ -92,17 +95,17 @@ export default class Config extends Command {
92
95
  // A detection failure (network hiccup, plugins REST disabled) is treated as "assume it's
93
96
  // there" rather than surprising the user with an account-creating prompt they didn't ask
94
97
  // for; `lps project config` can simply be re-run if it genuinely isn't installed.
95
- const alreadyActive = await isLoopressFullActive(wp).catch(() => true);
96
- if (alreadyActive)
98
+ const isAlreadyActive = await isLoopressFullActive(wp).catch(() => true);
99
+ if (isAlreadyActive)
97
100
  return;
98
- const proceed = await confirm({
101
+ const isProceed = await confirm({
99
102
  default: true,
100
103
  message: 'Loopress Full was not detected on this site. Install it now? (creates a temporary admin account, removed automatically afterward)',
101
104
  });
102
- if (!proceed)
105
+ if (!isProceed)
103
106
  return;
104
107
  try {
105
- await bootstrapLoopressFull(wp, url, (message) => this.log(message));
108
+ await bootstrapLoopressFull(wp, url, (message) => { this.log(message); });
106
109
  }
107
110
  catch (error) {
108
111
  this.warn(error.message);
@@ -137,7 +140,7 @@ export default class Config extends Command {
137
140
  return this.promptManualCredentials();
138
141
  }
139
142
  try {
140
- const { password, userLogin } = await authorizeWithBrowser(url, (message) => this.log(message));
143
+ const { password, userLogin } = await authorizeWithBrowser(url, (message) => { this.log(message); });
141
144
  return { appPassword: password, user: userLogin };
142
145
  }
143
146
  catch (error) {
@@ -1,6 +1,21 @@
1
1
  import { Command } from '@oclif/core';
2
+ type EnvironmentInfo = {
3
+ isCurrent: boolean;
4
+ name: string;
5
+ url: string;
6
+ };
7
+ type ProjectInfo = {
8
+ environments: EnvironmentInfo[];
9
+ id: string;
10
+ isCurrent: boolean;
11
+ name: string;
12
+ };
2
13
  export default class List extends Command {
3
14
  static description: string;
15
+ static enableJsonFlag: boolean;
4
16
  static examples: string[];
5
- run(): Promise<void>;
17
+ run(): Promise<ProjectInfo[]>;
18
+ private logEnvironment;
19
+ private logProject;
6
20
  }
21
+ export {};
@@ -3,28 +3,34 @@ import { configManager } from '../../config/project-config.manager.js';
3
3
  const c = ux.colorize;
4
4
  export default class List extends Command {
5
5
  static description = 'List configured WordPress projects';
6
+ static enableJsonFlag = true;
6
7
  static examples = ['$ lps project list'];
7
8
  async run() {
8
9
  await this.parse(List);
9
10
  const projects = configManager.listProjects();
10
11
  if (projects.length === 0) {
11
12
  this.log('No projects configured. Run `lps project config` first.');
12
- return;
13
- }
14
- for (const project of projects) {
15
- const envs = configManager.listEnvironments(project.id);
16
- const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
17
- const name = project.isCurrent ? c('green', project.name) : project.name;
18
- const currentTag = project.isCurrent ? ` ${c('green', '[current]')}` : '';
19
- this.log(`${marker} ${name}${currentTag}`);
20
- for (const env of envs) {
21
- const envMarker = env.isCurrent ? c('cyan', '·') : c('dim', '·');
22
- const envName = env.isCurrent ? c('cyan', env.name.padEnd(15)) : c('dim', env.name.padEnd(15));
23
- const envUrl = c('dim', env.url);
24
- const activeTag = env.isCurrent ? ` ${c('cyan', '←')}` : '';
25
- this.log(` ${envMarker} ${envName} ${envUrl}${activeTag}`);
26
- }
27
- this.log('');
13
+ return [];
28
14
  }
15
+ return projects.map((project) => this.logProject(project));
16
+ }
17
+ // Only name/url/isCurrent are surfaced, deliberately: EnvironmentConfig also carries the
18
+ // WordPress application password token, which must never end up in --json output.
19
+ logEnvironment(env) {
20
+ const envMarker = c(env.isCurrent ? 'cyan' : 'dim', '·');
21
+ const envName = c(env.isCurrent ? 'cyan' : 'dim', env.name.padEnd(15));
22
+ const envUrl = c('dim', env.url);
23
+ const activeTag = env.isCurrent ? ` ${c('cyan', '←')}` : '';
24
+ this.log(` ${envMarker} ${envName} ${envUrl}${activeTag}`);
25
+ return { isCurrent: env.isCurrent, name: env.name, url: env.url };
26
+ }
27
+ logProject(project) {
28
+ const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
29
+ const name = project.isCurrent ? c('green', project.name) : project.name;
30
+ const currentTag = project.isCurrent ? ` ${c('green', '[current]')}` : '';
31
+ this.log(`${marker} ${name}${currentTag}`);
32
+ const environments = configManager.listEnvironments(project.id).map((env) => this.logEnvironment(env));
33
+ this.log('');
34
+ return { environments, id: project.id, isCurrent: project.isCurrent, name: project.name };
29
35
  }
30
36
  }
@@ -3,6 +3,7 @@ import { Listr } from 'listr2';
3
3
  import { authManager } from '../../config/auth.manager.js';
4
4
  import { configManager } from '../../config/project-config.manager.js';
5
5
  import { ApiClient } from '../../lib/api-client.js';
6
+ import { pluralize } from '../../utils/pluralize.js';
6
7
  export default class Pull extends Command {
7
8
  static description = 'Pull projects and environments from your Loopress account that are not configured locally yet';
8
9
  static examples = ['$ lps project pull'];
@@ -31,7 +32,7 @@ export default class Pull extends Command {
31
32
  title: `Pull project "${apiProject.name}" from the API`,
32
33
  })), { concurrent: false, exitOnError: false }).run();
33
34
  }
34
- this.log(`\n✓ Pulled ${projectCount} project${projectCount === 1 ? '' : 's'}, ${environmentCount} environment${environmentCount === 1 ? '' : 's'} from your Loopress account`);
35
+ this.log(`\n✓ Pulled ${pluralize(projectCount, 'project')}, ${pluralize(environmentCount, 'environment')} from your Loopress account`);
35
36
  }
36
37
  async fetchApiProjects(api) {
37
38
  try {
@@ -64,6 +65,6 @@ export default class Pull extends Command {
64
65
  });
65
66
  const envCount = apiProject.environments.length;
66
67
  if (task)
67
- task.output = `Pulled with ${envCount} environment${envCount === 1 ? '' : 's'}`;
68
+ task.output = `Pulled with ${pluralize(envCount, 'environment')}`;
68
69
  }
69
70
  }
@@ -8,10 +8,15 @@ export default class Push extends Command {
8
8
  private yes;
9
9
  run(): Promise<void>;
10
10
  private applyEnvironment;
11
+ private applyEnvironmentPlans;
11
12
  private applyProject;
13
+ private applyProjectPlans;
14
+ private collectEnvsNeedingWork;
12
15
  private confirmLink;
13
16
  private fetchApiProjects;
17
+ private planAll;
14
18
  private planEnvironment;
15
19
  private planProject;
20
+ private pushAllCredentials;
16
21
  private pushCredentials;
17
22
  }