@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.
- package/README.md +254 -75
- package/dist/commands/acf/list.d.ts +2 -2
- package/dist/commands/acf/list.js +4 -6
- package/dist/commands/acf/pull.js +9 -22
- package/dist/commands/acf/push.js +4 -7
- package/dist/commands/api/list.d.ts +7 -4
- package/dist/commands/api/list.js +5 -11
- package/dist/commands/api/publish.js +7 -23
- package/dist/commands/api/pull.d.ts +8 -1
- package/dist/commands/api/pull.js +22 -18
- package/dist/commands/api/push.d.ts +7 -1
- package/dist/commands/api/push.js +50 -23
- package/dist/commands/composer/init.js +6 -6
- package/dist/commands/composer/pull.d.ts +7 -1
- package/dist/commands/composer/pull.js +4 -2
- package/dist/commands/composer/push.d.ts +8 -1
- package/dist/commands/composer/push.js +3 -1
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +97 -0
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/form/list.d.ts +2 -4
- package/dist/commands/form/list.js +3 -10
- package/dist/commands/form/pull.js +15 -24
- package/dist/commands/form/push.js +17 -42
- package/dist/commands/init.js +7 -7
- package/dist/commands/login.js +9 -5
- package/dist/commands/logout.js +2 -1
- package/dist/commands/page/list.d.ts +2 -4
- package/dist/commands/page/list.js +3 -10
- package/dist/commands/page/pull.d.ts +13 -1
- package/dist/commands/page/pull.js +21 -29
- package/dist/commands/page/push.d.ts +11 -1
- package/dist/commands/page/push.js +31 -49
- package/dist/commands/plugin/add.js +3 -3
- package/dist/commands/plugin/pull.d.ts +8 -1
- package/dist/commands/plugin/pull.js +11 -11
- package/dist/commands/plugin/push.d.ts +12 -1
- package/dist/commands/plugin/push.js +41 -27
- package/dist/commands/project/config.js +12 -9
- package/dist/commands/project/list.d.ts +16 -1
- package/dist/commands/project/list.js +22 -16
- package/dist/commands/project/pull.js +3 -2
- package/dist/commands/project/push.d.ts +5 -0
- package/dist/commands/project/push.js +84 -63
- package/dist/commands/project/switch.js +2 -2
- package/dist/commands/promote.d.ts +18 -0
- package/dist/commands/promote.js +90 -0
- package/dist/commands/pull.d.ts +12 -0
- package/dist/commands/pull.js +51 -0
- package/dist/commands/push.d.ts +13 -0
- package/dist/commands/push.js +57 -0
- package/dist/commands/seo/list.d.ts +9 -2
- package/dist/commands/seo/list.js +9 -11
- package/dist/commands/seo/pull.js +14 -37
- package/dist/commands/seo/push.js +24 -48
- package/dist/commands/snippet/list.d.ts +3 -4
- package/dist/commands/snippet/list.js +5 -11
- package/dist/commands/snippet/publish.js +5 -21
- package/dist/commands/snippet/pull.d.ts +13 -1
- package/dist/commands/snippet/pull.js +18 -20
- package/dist/commands/snippet/push.d.ts +11 -1
- package/dist/commands/snippet/push.js +29 -28
- package/dist/commands/status.d.ts +13 -1
- package/dist/commands/status.js +25 -18
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +24 -0
- package/dist/config/auth.manager.d.ts +2 -2
- package/dist/config/project-config.manager.d.ts +1 -1
- package/dist/config/project-config.manager.js +5 -6
- package/dist/lib/base.d.ts +11 -2
- package/dist/lib/base.js +54 -24
- package/dist/lib/bootstrap-full-install.d.ts +1 -1
- package/dist/lib/dev-watch.d.ts +16 -0
- package/dist/lib/dev-watch.js +71 -0
- package/dist/lib/find-orphaned-files.d.ts +3 -2
- package/dist/lib/find-orphaned-files.js +40 -4
- package/dist/lib/github-release.js +1 -1
- package/dist/lib/guard-production-push.d.ts +12 -0
- package/dist/lib/guard-production-push.js +20 -0
- package/dist/lib/interactive.js +1 -1
- package/dist/lib/load-files.d.ts +1 -0
- package/dist/lib/load-files.js +40 -11
- package/dist/lib/load-snippets.d.ts +1 -1
- package/dist/lib/load-snippets.js +66 -59
- package/dist/lib/local-callback-server.js +15 -8
- package/dist/lib/plugin-detection.d.ts +1 -1
- package/dist/lib/push-command.d.ts +3 -0
- package/dist/lib/push-command.js +22 -15
- package/dist/lib/put-or-create.d.ts +13 -0
- package/dist/lib/put-or-create.js +19 -0
- package/dist/lib/readdir-tolerant.d.ts +5 -0
- package/dist/lib/readdir-tolerant.js +11 -0
- package/dist/lib/resolve-linked-project.d.ts +8 -0
- package/dist/lib/resolve-linked-project.js +27 -0
- package/dist/lib/rotate-app-password.d.ts +1 -1
- package/dist/lib/rotate-app-password.js +1 -1
- package/dist/lib/temp-admin.d.ts +3 -3
- package/dist/lib/temp-admin.js +2 -2
- package/dist/lib/validate-local.d.ts +10 -0
- package/dist/lib/validate-local.js +183 -0
- package/dist/lib/wp-authorize-flow.js +5 -1
- package/dist/lib/wp-client.d.ts +2 -2
- package/dist/lib/wp-client.js +7 -0
- package/dist/types/plugin.d.ts +4 -4
- package/dist/types/snippet.d.ts +3 -3
- package/dist/utils/composer.d.ts +2 -2
- package/dist/utils/form-format.js +1 -1
- package/dist/utils/loopress-config.d.ts +1 -1
- package/dist/utils/page-format.js +2 -2
- package/dist/utils/plugins.d.ts +5 -5
- package/dist/utils/plugins.js +3 -3
- package/dist/utils/pluralize.d.ts +1 -0
- package/dist/utils/pluralize.js +5 -0
- package/dist/utils/seo-format.d.ts +6 -6
- package/dist/utils/snippet-format.d.ts +2 -2
- package/dist/utils/snippet-format.js +10 -5
- package/oclif.manifest.json +394 -57
- package/package.json +18 -16
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
21
|
+
const isUpdated = existing[slug] !== undefined;
|
|
22
22
|
if (this.dryRun) {
|
|
23
|
-
this.log(`[dry-run] Would ${
|
|
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(`${
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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<
|
|
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
|
|
22
|
-
|
|
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
|
|
39
|
+
const isOverwrite = await confirm({
|
|
40
40
|
default: false,
|
|
41
41
|
message: `"${projectName}/${envName}" already exists. Overwrite?`,
|
|
42
42
|
});
|
|
43
|
-
if (!
|
|
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
|
-
|
|
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
|
|
96
|
-
if (
|
|
98
|
+
const isAlreadyActive = await isLoopressFullActive(wp).catch(() => true);
|
|
99
|
+
if (isAlreadyActive)
|
|
97
100
|
return;
|
|
98
|
-
const
|
|
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 (!
|
|
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<
|
|
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
|
|
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
|
|
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
|
}
|