@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.
- package/README.md +1369 -360
- package/dist/commands/acf/push.js +2 -1
- package/dist/commands/composer/push.d.ts +1 -0
- package/dist/commands/composer/push.js +13 -0
- package/dist/commands/content/copy.d.ts +28 -0
- package/dist/commands/content/copy.js +97 -0
- package/dist/commands/cpt/diff.js +5 -0
- package/dist/commands/cpt/list.d.ts +2 -0
- package/dist/commands/cpt/list.js +4 -0
- package/dist/commands/cpt/pull.d.ts +2 -0
- package/dist/commands/cpt/pull.js +4 -0
- package/dist/commands/cpt/push.d.ts +2 -0
- package/dist/commands/cpt/push.js +4 -0
- package/dist/commands/cpt/rm.d.ts +2 -0
- package/dist/commands/cpt/rm.js +3 -0
- package/dist/commands/cpt/rollback.js +5 -0
- package/dist/commands/dev.js +14 -4
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +57 -2
- package/dist/commands/form/push.js +4 -1
- package/dist/commands/meta/diff.d.ts +2 -0
- package/dist/commands/meta/diff.js +5 -0
- package/dist/commands/meta/list.d.ts +19 -0
- package/dist/commands/meta/list.js +21 -0
- package/dist/commands/meta/pull.d.ts +2 -0
- package/dist/commands/meta/pull.js +4 -0
- package/dist/commands/meta/push.d.ts +2 -0
- package/dist/commands/meta/push.js +4 -0
- package/dist/commands/meta/rm.d.ts +2 -0
- package/dist/commands/meta/rm.js +3 -0
- package/dist/commands/meta/rollback.d.ts +2 -0
- package/dist/commands/meta/rollback.js +5 -0
- package/dist/commands/option/list.js +2 -8
- package/dist/commands/option/push.js +4 -2
- package/dist/commands/page/push.js +3 -2
- package/dist/commands/pattern/diff.d.ts +2 -0
- package/dist/commands/pattern/diff.js +5 -0
- package/dist/commands/pattern/list.d.ts +2 -0
- package/dist/commands/pattern/list.js +3 -0
- package/dist/commands/pattern/pull.d.ts +2 -0
- package/dist/commands/pattern/pull.js +4 -0
- package/dist/commands/pattern/push.d.ts +2 -0
- package/dist/commands/pattern/push.js +4 -0
- package/dist/commands/pattern/rm.d.ts +2 -0
- package/dist/commands/pattern/rm.js +3 -0
- package/dist/commands/pattern/rollback.d.ts +2 -0
- package/dist/commands/pattern/rollback.js +5 -0
- package/dist/commands/plugin/audit.js +43 -25
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +20 -1
- package/dist/commands/project/list.d.ts +1 -1
- package/dist/commands/project/list.js +6 -6
- package/dist/commands/project/pull.js +11 -8
- package/dist/commands/project/push.js +23 -18
- package/dist/commands/project/remove.js +1 -1
- package/dist/commands/project/switch.d.ts +0 -1
- package/dist/commands/project/switch.js +16 -36
- package/dist/commands/promote.d.ts +1 -0
- package/dist/commands/promote.js +7 -3
- package/dist/commands/pull.d.ts +2 -1
- package/dist/commands/pull.js +31 -6
- package/dist/commands/push.d.ts +4 -2
- package/dist/commands/push.js +51 -19
- package/dist/commands/role/diff.d.ts +2 -0
- package/dist/commands/role/diff.js +5 -0
- package/dist/commands/role/list.d.ts +2 -0
- package/dist/commands/role/list.js +4 -0
- package/dist/commands/role/pull.d.ts +2 -0
- package/dist/commands/role/pull.js +4 -0
- package/dist/commands/role/push.d.ts +2 -0
- package/dist/commands/role/push.js +4 -0
- package/dist/commands/role/rm.d.ts +2 -0
- package/dist/commands/role/rm.js +3 -0
- package/dist/commands/role/rollback.d.ts +2 -0
- package/dist/commands/role/rollback.js +5 -0
- package/dist/commands/snippet/push.d.ts +1 -0
- package/dist/commands/snippet/push.js +24 -3
- package/dist/commands/status.d.ts +2 -3
- package/dist/commands/status.js +21 -53
- package/dist/commands/taxonomy/diff.d.ts +2 -0
- package/dist/commands/taxonomy/diff.js +5 -0
- package/dist/commands/taxonomy/list.d.ts +2 -0
- package/dist/commands/taxonomy/list.js +5 -0
- package/dist/commands/taxonomy/pull.d.ts +2 -0
- package/dist/commands/taxonomy/pull.js +4 -0
- package/dist/commands/taxonomy/push.d.ts +2 -0
- package/dist/commands/taxonomy/push.js +4 -0
- package/dist/commands/taxonomy/rm.d.ts +2 -0
- package/dist/commands/taxonomy/rm.js +3 -0
- package/dist/commands/taxonomy/rollback.d.ts +2 -0
- package/dist/commands/taxonomy/rollback.js +5 -0
- package/dist/commands/theme/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +26 -5
- package/dist/commands/theme/version/pull.js +6 -1
- package/dist/config/project-config.manager.d.ts +3 -3
- package/dist/config/project-config.manager.js +22 -31
- package/dist/hooks/finally.js +11 -3
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/app-build.js +3 -1
- package/dist/lib/base.d.ts +2 -2
- package/dist/lib/base.js +33 -38
- package/dist/lib/declared-type-command.d.ts +35 -0
- package/dist/lib/declared-type-command.js +192 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +7 -3
- package/dist/lib/diff-command.d.ts +2 -0
- package/dist/lib/diff-command.js +24 -3
- package/dist/lib/github-release.js +4 -0
- package/dist/lib/load-snippets.js +4 -0
- package/dist/lib/local-callback-server.js +3 -1
- package/dist/lib/php-files-command.d.ts +8 -0
- package/dist/lib/php-files-command.js +5 -0
- package/dist/lib/push-command.d.ts +5 -2
- package/dist/lib/push-command.js +96 -15
- package/dist/lib/put-or-create.js +3 -4
- package/dist/lib/resource-state.js +38 -85
- package/dist/lib/rollback-command.js +9 -2
- package/dist/lib/rollback-materialize.js +13 -24
- package/dist/lib/spinner.d.ts +2 -0
- package/dist/lib/spinner.js +72 -0
- package/dist/lib/temp-admin.d.ts +10 -1
- package/dist/lib/temp-admin.js +27 -2
- package/dist/lib/validate-local.js +40 -12
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +21 -2
- package/dist/lib/wp-site-diagnostic.js +3 -2
- package/dist/types/global-config.generated.d.ts +1 -5
- package/dist/types/project-config.generated.d.ts +28 -4
- package/dist/types/snippet.d.ts +1 -0
- package/dist/utils/composer.js +2 -1
- package/dist/utils/declared-type-format.d.ts +52 -0
- package/dist/utils/declared-type-format.js +233 -0
- package/dist/utils/resource-dirs.d.ts +9 -3
- package/dist/utils/resource-dirs.js +16 -4
- package/dist/utils/template-format.d.ts +1 -0
- package/dist/utils/theme-css.d.ts +19 -0
- package/dist/utils/theme-css.js +100 -0
- package/oclif.manifest.json +3294 -1213
- package/package.json +32 -16
- package/schemas/global-config.schema.json +2 -6
- package/schemas/project-config.schema.json +37 -7
- package/dist/commands/seo/diff.js +0 -5
- package/dist/commands/seo/list.d.ts +0 -18
- package/dist/commands/seo/list.js +0 -58
- package/dist/commands/seo/pull.d.ts +0 -24
- package/dist/commands/seo/pull.js +0 -89
- package/dist/commands/seo/push.d.ts +0 -29
- package/dist/commands/seo/push.js +0 -178
- package/dist/commands/seo/rollback.js +0 -5
- package/dist/utils/seo-format.d.ts +0 -29
- package/dist/utils/seo-format.js +0 -18
- /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
- /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
3
4
|
import { pinVersion } from '../../types/plugin.js';
|
|
4
5
|
import { compareVersions } from '../../utils/version.js';
|
|
5
6
|
const WPVULN_BASE = 'https://www.wpvulnerability.net/plugin';
|
|
@@ -19,7 +20,7 @@ export default class Audit extends LoopressCommand {
|
|
|
19
20
|
const advisories = [];
|
|
20
21
|
const health = [];
|
|
21
22
|
for (const slug of slugs) {
|
|
22
|
-
const found = await this.auditOne(slug, pinVersion(manifest[slug]));
|
|
23
|
+
const found = await withSpinner(`Auditing ${slug}`, async () => this.auditOne(slug, pinVersion(manifest[slug])));
|
|
23
24
|
advisories.push(...found.advisories);
|
|
24
25
|
health.push(...found.health);
|
|
25
26
|
}
|
|
@@ -27,7 +28,12 @@ export default class Audit extends LoopressCommand {
|
|
|
27
28
|
}
|
|
28
29
|
async auditOne(slug, pinned) {
|
|
29
30
|
const [vulns, info] = await Promise.all([this.fetchVulnerabilities(slug), this.fetchHealth(slug)]);
|
|
30
|
-
|
|
31
|
+
// "latest" is the newest WordPress.org release, the one a push installs: audit that version.
|
|
32
|
+
// Unknown (WordPress.org unreachable), every advisory is reported, safer than none.
|
|
33
|
+
const version = pinned === 'latest' && typeof info?.version === 'string' ? info.version : pinned;
|
|
34
|
+
const advisories = vulns
|
|
35
|
+
.filter((v) => version === 'latest' || affects(version, v.range))
|
|
36
|
+
.map(({ range: _range, ...v }) => ({ ...v, slug }));
|
|
31
37
|
const health = info ? describeHealth(slug, pinned, info) : [];
|
|
32
38
|
return { advisories, health };
|
|
33
39
|
}
|
|
@@ -71,8 +77,13 @@ export default class Audit extends LoopressCommand {
|
|
|
71
77
|
const noun = advisories.length === 1 ? 'advisory' : 'advisories';
|
|
72
78
|
this.log(`\n${advisories.length} vulnerability ${noun}, ${health.length} health note(s). ` +
|
|
73
79
|
'Vulnerability data: wpvulnerability.net.');
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
// exit() throws before oclif prints the --json result, which a CI reading it needs most here.
|
|
81
|
+
if (advisories.length > 0) {
|
|
82
|
+
if (this.jsonEnabled())
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
else
|
|
85
|
+
this.exit(1);
|
|
86
|
+
}
|
|
76
87
|
return { advisories, health, status: 'issues' };
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -85,34 +96,41 @@ function normalizeAdvisory(raw) {
|
|
|
85
96
|
const cvss = (raw.cvss ?? {});
|
|
86
97
|
const source = Array.isArray(raw.source) ? raw.source : [];
|
|
87
98
|
const cve = typeof raw.cve === 'string' ? raw.cve : source.find((s) => s.id?.startsWith('CVE-'))?.id;
|
|
99
|
+
const range = extractRange(raw);
|
|
88
100
|
return {
|
|
89
101
|
cve,
|
|
90
|
-
fixedIn:
|
|
102
|
+
fixedIn: range.max?.op === 'lt' ? range.max.version : undefined,
|
|
103
|
+
range,
|
|
91
104
|
score: cvss.score,
|
|
92
105
|
severity: cvss.severity,
|
|
93
|
-
title: typeof raw.name === 'string' ? raw.name : 'Unnamed advisory',
|
|
106
|
+
title: typeof raw.name === 'string' ? decodeEntities(raw.name) : 'Unnamed advisory',
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
|
-
// wpvulnerability's `
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
function
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
// wpvulnerability's `operator`: `max_version` with `max_operator` "lt" (fixed in that version),
|
|
110
|
+
// "le" (affected up to it, no fix yet) or "eq" (that version only), and an optional lower bound `min_version` with
|
|
111
|
+
// `min_operator` "gt" or "ge". No upper bound means every version from the lower one on.
|
|
112
|
+
function extractRange(raw) {
|
|
113
|
+
const op = (raw.operator ?? {});
|
|
114
|
+
const bound = (version, operator) => typeof version === 'string' && version !== '' && typeof operator === 'string' ? { op: operator, version } : undefined;
|
|
115
|
+
return { max: bound(op.max_version, op.max_operator), min: bound(op.min_version, op.min_operator) };
|
|
116
|
+
}
|
|
117
|
+
// A bound we can't evaluate (odd version strings, unknown operator) counts as affected: a false
|
|
118
|
+
// positive is safer than hiding an advisory.
|
|
119
|
+
function affects(version, range) {
|
|
120
|
+
const within = (bound, ok) => {
|
|
121
|
+
if (!bound)
|
|
122
|
+
return true;
|
|
123
|
+
const order = compareVersions(version, bound.version);
|
|
124
|
+
return order === null || !Object.hasOwn(ok, bound.op) || ok[bound.op](order);
|
|
125
|
+
};
|
|
126
|
+
return (within(range.min, { ge: (o) => o >= 0, gt: (o) => o > 0 }) && within(range.max, { eq: (o) => o === 0, le: (o) => o <= 0, lt: (o) => o < 0 }));
|
|
108
127
|
}
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return order === null || order < 0;
|
|
128
|
+
// Advisory names come HTML-encoded ("WPForms – AI Form Builder").
|
|
129
|
+
function decodeEntities(text) {
|
|
130
|
+
return text
|
|
131
|
+
.replaceAll(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
132
|
+
.replaceAll('"', '"')
|
|
133
|
+
.replaceAll('&', '&');
|
|
116
134
|
}
|
|
117
135
|
function describeHealth(slug, pinned, info) {
|
|
118
136
|
const notes = [];
|
|
@@ -4,9 +4,12 @@ import { configManager } from '../../config/project-config.manager.js';
|
|
|
4
4
|
import { bootstrapLoopressFull } from '../../lib/bootstrap-full-install.js';
|
|
5
5
|
import { isInteractive } from '../../lib/interactive.js';
|
|
6
6
|
import { isLoopressFullActive } from '../../lib/plugin-detection.js';
|
|
7
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
8
|
+
import { sweepTempAdmins } from '../../lib/temp-admin.js';
|
|
7
9
|
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
8
10
|
import { isLocalEnvironment, WpClient } from '../../lib/wp-client.js';
|
|
9
11
|
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
12
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
10
13
|
const NEW_PROJECT = '__new__';
|
|
11
14
|
const AUTH_BROWSER = 'browser';
|
|
12
15
|
const AUTH_MANUAL = 'manual';
|
|
@@ -96,6 +99,10 @@ export default class Config extends Command {
|
|
|
96
99
|
if (!token)
|
|
97
100
|
return; // env was just built with one a few lines up; guard only narrows the type
|
|
98
101
|
const wp = new WpClient(url, token, name);
|
|
102
|
+
// Before the "already active" early return: an install killed mid-way (SIGKILL, power loss)
|
|
103
|
+
// may have activated Full and still left its temp admin behind, and re-running this command
|
|
104
|
+
// is the documented fix for both.
|
|
105
|
+
await this.sweepLeftoverTempAdmins(wp);
|
|
99
106
|
// A detection failure (network hiccup, plugins REST disabled) is treated as "assume it's
|
|
100
107
|
// there" rather than surprising the user with an account-creating prompt they didn't ask
|
|
101
108
|
// for; `lps project config` can simply be re-run if it genuinely isn't installed.
|
|
@@ -109,7 +116,9 @@ export default class Config extends Command {
|
|
|
109
116
|
if (!isProceed)
|
|
110
117
|
return;
|
|
111
118
|
try {
|
|
112
|
-
await bootstrapLoopressFull(wp, url, (message) => {
|
|
119
|
+
await withSpinner('Installing Loopress Full', async () => bootstrapLoopressFull(wp, url, (message) => {
|
|
120
|
+
this.log(message);
|
|
121
|
+
}));
|
|
113
122
|
}
|
|
114
123
|
catch (error) {
|
|
115
124
|
this.warn(error.message);
|
|
@@ -181,4 +190,14 @@ export default class Config extends Command {
|
|
|
181
190
|
});
|
|
182
191
|
return { projectId: configManager.createProjectId(projectName.trim()), projectName: projectName.trim() };
|
|
183
192
|
}
|
|
193
|
+
async sweepLeftoverTempAdmins(wp) {
|
|
194
|
+
try {
|
|
195
|
+
const removed = await sweepTempAdmins(wp);
|
|
196
|
+
if (removed.length > 0)
|
|
197
|
+
this.log(`✓ Removed ${pluralize(removed.length, 'leftover temporary admin account')}: ${removed.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
this.warn(error.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
184
203
|
}
|
|
@@ -14,15 +14,15 @@ export default class List extends Command {
|
|
|
14
14
|
}
|
|
15
15
|
return projects.map((project) => this.logProject(project));
|
|
16
16
|
}
|
|
17
|
-
// Only name/url/
|
|
17
|
+
// Only name/url/isDefault are surfaced, deliberately: EnvironmentConfig also carries the
|
|
18
18
|
// WordPress application password token, which must never end up in --json output.
|
|
19
19
|
logEnvironment(env) {
|
|
20
|
-
const envMarker = c(env.
|
|
21
|
-
const envName = c(env.
|
|
20
|
+
const envMarker = c(env.isDefault ? 'cyan' : 'dim', '·');
|
|
21
|
+
const envName = c(env.isDefault ? 'cyan' : 'dim', env.name.padEnd(15));
|
|
22
22
|
const envUrl = c('dim', env.url);
|
|
23
|
-
const
|
|
24
|
-
this.log(` ${envMarker} ${envName} ${envUrl}${
|
|
25
|
-
return {
|
|
23
|
+
const defaultTag = env.isDefault ? ` ${c('cyan', '[default]')}` : '';
|
|
24
|
+
this.log(` ${envMarker} ${envName} ${envUrl}${defaultTag}`);
|
|
25
|
+
return { isDefault: env.isDefault, name: env.name, url: env.url };
|
|
26
26
|
}
|
|
27
27
|
logProject(project) {
|
|
28
28
|
const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
|
|
@@ -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 { withoutSpinner } from '../../lib/spinner.js';
|
|
6
7
|
import { pluralize } from '../../utils/pluralize.js';
|
|
7
8
|
export default class Pull extends Command {
|
|
8
9
|
static description = 'Pull projects and environments from your Loopress account that are not configured locally yet';
|
|
@@ -23,14 +24,16 @@ export default class Pull extends Command {
|
|
|
23
24
|
let projectCount = 0;
|
|
24
25
|
let environmentCount = 0;
|
|
25
26
|
if (newApiProjects.length > 0) {
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
await withoutSpinner(async () => {
|
|
28
|
+
await new Listr(newApiProjects.map((apiProject) => ({
|
|
29
|
+
task: (_ctx, task) => {
|
|
30
|
+
this.pullProject(apiProject, task);
|
|
31
|
+
projectCount++;
|
|
32
|
+
environmentCount += apiProject.environments.length;
|
|
33
|
+
},
|
|
34
|
+
title: `Pull project "${apiProject.name}" from the API`,
|
|
35
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
36
|
+
});
|
|
34
37
|
}
|
|
35
38
|
this.log(`\n✓ Pulled ${pluralize(projectCount, 'project')}, ${pluralize(environmentCount, 'environment')} from your Loopress account`);
|
|
36
39
|
}
|
|
@@ -6,6 +6,7 @@ import { configManager } from '../../config/project-config.manager.js';
|
|
|
6
6
|
import { ApiClient } from '../../lib/api-client.js';
|
|
7
7
|
import { LoopressCommand } from '../../lib/base.js';
|
|
8
8
|
import { isInteractive } from '../../lib/interactive.js';
|
|
9
|
+
import { withoutSpinner } from '../../lib/spinner.js';
|
|
9
10
|
import { pluralize } from '../../utils/pluralize.js';
|
|
10
11
|
import { toSlug } from '../../utils/to-slug.js';
|
|
11
12
|
export default class Push extends Command {
|
|
@@ -66,15 +67,17 @@ export default class Push extends Command {
|
|
|
66
67
|
if (envsNeedingWork.length === 0)
|
|
67
68
|
return 0;
|
|
68
69
|
let count = 0;
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
await withoutSpinner(async () => {
|
|
71
|
+
await new Listr(envsNeedingWork.map(({ apiProjectId, envPlan, projectName }) => ({
|
|
72
|
+
task: async (_ctx, task) => {
|
|
73
|
+
await this.applyEnvironment(api, apiProjectId, envPlan, task);
|
|
74
|
+
count++;
|
|
75
|
+
},
|
|
76
|
+
title: envPlan.action === 'create'
|
|
77
|
+
? `Create environment "${envPlan.env.name}" on "${projectName}"`
|
|
78
|
+
: `Link environment "${envPlan.env.name}" on "${projectName}"`,
|
|
79
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
80
|
+
});
|
|
78
81
|
return count;
|
|
79
82
|
}
|
|
80
83
|
async applyProject(api, plan, task) {
|
|
@@ -101,15 +104,17 @@ export default class Push extends Command {
|
|
|
101
104
|
if (projectsNeedingWork.length === 0)
|
|
102
105
|
return 0;
|
|
103
106
|
let count = 0;
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
await withoutSpinner(async () => {
|
|
108
|
+
await new Listr(projectsNeedingWork.map((plan) => ({
|
|
109
|
+
task: async (_ctx, task) => {
|
|
110
|
+
await this.applyProject(api, plan, task);
|
|
111
|
+
count++;
|
|
112
|
+
},
|
|
113
|
+
title: plan.action === 'create'
|
|
114
|
+
? `Create project "${plan.project.name}" on the API`
|
|
115
|
+
: `Link project "${plan.project.name}" to the API`,
|
|
116
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
117
|
+
});
|
|
113
118
|
return count;
|
|
114
119
|
}
|
|
115
120
|
collectEnvsNeedingWork(projectPlans, envPlansByProject) {
|
|
@@ -24,7 +24,7 @@ export default class Remove extends Command {
|
|
|
24
24
|
const envChoices = configManager.listEnvironments(project.id).map((env) => {
|
|
25
25
|
targets.push({ env: env.name, kind: 'env', projectId: project.id, projectName: project.name });
|
|
26
26
|
return {
|
|
27
|
-
name: ` ${env.name.padEnd(20)} ${env.url}
|
|
27
|
+
name: ` ${env.name.padEnd(20)} ${env.url}`,
|
|
28
28
|
value: String(targets.length - 1),
|
|
29
29
|
};
|
|
30
30
|
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { select
|
|
2
|
-
import { Command
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
-
const c = ux.colorize;
|
|
5
4
|
export default class Switch extends Command {
|
|
6
|
-
static description = 'Switch the active project
|
|
5
|
+
static description = 'Switch the active project (pass --env to target a non-local environment)';
|
|
7
6
|
static examples = ['$ lps project switch'];
|
|
8
7
|
async run() {
|
|
9
8
|
await this.parse(Switch);
|
|
@@ -11,37 +10,18 @@ export default class Switch extends Command {
|
|
|
11
10
|
if (projects.length === 0) {
|
|
12
11
|
this.error('No projects configured. Run `lps project config` first.');
|
|
13
12
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const { env, project } = entries[0];
|
|
28
|
-
return { envName: env.name, projectId: project.id, projectName: project.name };
|
|
29
|
-
}
|
|
30
|
-
const choices = groups.flatMap(({ envs, project }) => [
|
|
31
|
-
new Separator(c(project.isCurrent ? 'green' : 'dim', `─── ${project.name} ───`)),
|
|
32
|
-
...envs.map((env) => ({
|
|
33
|
-
name: `${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
34
|
-
value: `${project.id}::${env.name}`,
|
|
35
|
-
})),
|
|
36
|
-
]);
|
|
37
|
-
const current = entries.find(({ env }) => env.isCurrent);
|
|
38
|
-
const chosen = await select({
|
|
39
|
-
choices,
|
|
40
|
-
default: current && `${current.project.id}::${current.env.name}`,
|
|
41
|
-
message: 'Select project / environment',
|
|
42
|
-
});
|
|
43
|
-
const [projectId, envName] = chosen.split('::', 2);
|
|
44
|
-
const { project } = groups.find((group) => group.project.id === projectId);
|
|
45
|
-
return { envName, projectId, projectName: project.name };
|
|
13
|
+
const projectId = projects.length === 1
|
|
14
|
+
? projects[0].id
|
|
15
|
+
: await select({
|
|
16
|
+
choices: projects.map((project) => ({
|
|
17
|
+
name: `${project.name}${project.isCurrent ? ' [current]' : ''}`,
|
|
18
|
+
value: project.id,
|
|
19
|
+
})),
|
|
20
|
+
default: projects.find((project) => project.isCurrent)?.id,
|
|
21
|
+
message: 'Select project',
|
|
22
|
+
});
|
|
23
|
+
configManager.setCurrent(projectId);
|
|
24
|
+
const project = projects.find((candidate) => candidate.id === projectId);
|
|
25
|
+
this.log(`✓ Switched to "${project.name}"`);
|
|
46
26
|
}
|
|
47
27
|
}
|
|
@@ -13,6 +13,7 @@ export default class Promote extends Command {
|
|
|
13
13
|
static enableJsonFlag: boolean;
|
|
14
14
|
static examples: string[];
|
|
15
15
|
static flags: {
|
|
16
|
+
overwrite: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
19
|
};
|
package/dist/commands/promote.js
CHANGED
|
@@ -8,6 +8,7 @@ import { configManager } from '../config/project-config.manager.js';
|
|
|
8
8
|
import { LoopressCommand } from '../lib/base.js';
|
|
9
9
|
import { isInteractive } from '../lib/interactive.js';
|
|
10
10
|
import { stdoutToStderr } from '../lib/json-delegation.js';
|
|
11
|
+
import { PushCommand } from '../lib/push-command.js';
|
|
11
12
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
12
13
|
import { resolveResourceDir, RESOURCE_DIR_DEFAULTS } from '../utils/resource-dirs.js';
|
|
13
14
|
export default class Promote extends Command {
|
|
@@ -21,10 +22,11 @@ export default class Promote extends Command {
|
|
|
21
22
|
static flags = {
|
|
22
23
|
...LoopressCommand.dryRunFlag,
|
|
23
24
|
...LoopressCommand.yesFlag,
|
|
25
|
+
overwrite: PushCommand.baseFlags.overwrite,
|
|
24
26
|
};
|
|
25
27
|
async run() {
|
|
26
28
|
const { args, flags } = await this.parse(Promote);
|
|
27
|
-
const { yes } = flags;
|
|
29
|
+
const { overwrite, yes } = flags;
|
|
28
30
|
const dryRun = flags['dry-run'];
|
|
29
31
|
const { environments, projectName } = await this.resolveProject();
|
|
30
32
|
const from = this.requireEnvironment(environments, projectName, args.from);
|
|
@@ -52,10 +54,12 @@ export default class Promote extends Command {
|
|
|
52
54
|
}
|
|
53
55
|
catch (error) {
|
|
54
56
|
// A partial pull must never be pushed onward: stop before touching <to>.
|
|
55
|
-
|
|
57
|
+
// "untouched" is only true of <to>: a real (non dry-run) pull has already rewritten the files.
|
|
58
|
+
const filesNote = dryRun ? '' : ` Your local files may already hold part of ${from.name}, review them before pushing.`;
|
|
59
|
+
this.error(`Pull from ${from.name} failed, nothing was pushed to ${to.name}.${filesNote} ${error.message}`);
|
|
56
60
|
}
|
|
57
61
|
this.log(`\n=== Pushing to ${to.name} (${to.url}) ===`);
|
|
58
|
-
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand('push', this.delegateArgv(to.name, dryRun)));
|
|
62
|
+
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand('push', [...this.delegateArgv(to.name, dryRun), ...(overwrite ? ['--overwrite'] : [])]));
|
|
59
63
|
}
|
|
60
64
|
finally {
|
|
61
65
|
if (scratch) {
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LoopressCommand } from '../lib/base.js';
|
|
|
2
2
|
type PullTargetResult = {
|
|
3
3
|
error?: string;
|
|
4
4
|
label: string;
|
|
5
|
-
status: 'failed' | 'pulled';
|
|
5
|
+
status: 'failed' | 'pulled' | 'skipped';
|
|
6
6
|
};
|
|
7
7
|
type PullResult = {
|
|
8
8
|
results: PullTargetResult[];
|
|
@@ -18,5 +18,6 @@ export default class Pull extends LoopressCommand {
|
|
|
18
18
|
private failedCount;
|
|
19
19
|
run(): Promise<PullResult>;
|
|
20
20
|
private buildArgv;
|
|
21
|
+
private usesResource;
|
|
21
22
|
}
|
|
22
23
|
export {};
|
package/dist/commands/pull.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { LoopressCommand } from '../lib/base.js';
|
|
2
4
|
import { stdoutToStderr } from '../lib/json-delegation.js';
|
|
3
5
|
import { pluralize } from '../utils/pluralize.js';
|
|
6
|
+
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
7
|
+
// How the plugin reports its integration missing: "ACF is not active", "No supported form
|
|
8
|
+
// plugin is active", "No supported snippet plugin is active".
|
|
9
|
+
const PLUGIN_MISSING = /\bis not active\b|\bNo supported .+ plugin is active\b/;
|
|
4
10
|
// composer before plugins: when a composer.json exists, `plugin:pull` merges the live plugins
|
|
5
11
|
// into it, so it must run on the freshly pulled one or `composer:pull` would overwrite its work. The rest write to independent
|
|
6
12
|
// directories and their order does not matter, it mirrors `lps push` for familiarity.
|
|
7
13
|
const PULL_TARGETS = [
|
|
8
14
|
{ commandId: 'composer:pull', label: 'composer' },
|
|
9
15
|
{ commandId: 'plugin:pull', label: 'plugins' },
|
|
10
|
-
{ commandId: '
|
|
16
|
+
{ commandId: 'cpt:pull', label: 'post types', supportsYes: true },
|
|
17
|
+
{ commandId: 'taxonomy:pull', label: 'taxonomies', supportsYes: true },
|
|
18
|
+
{ commandId: 'meta:pull', label: 'post meta', supportsYes: true },
|
|
19
|
+
{ commandId: 'role:pull', label: 'roles', supportsYes: true },
|
|
20
|
+
{ commandId: 'acf:pull', label: 'ACF', needsPlugin: 'acf', supportsYes: true },
|
|
11
21
|
{ commandId: 'api:pull', label: 'API routes', supportsYes: true },
|
|
12
22
|
{ commandId: 'hook:pull', label: 'hooks', supportsYes: true },
|
|
13
|
-
{ commandId: 'form:pull', label: 'forms', supportsYes: true },
|
|
14
|
-
{ commandId: '
|
|
23
|
+
{ commandId: 'form:pull', label: 'forms', needsPlugin: 'form', supportsYes: true },
|
|
24
|
+
{ commandId: 'pattern:pull', label: 'patterns', supportsYes: true },
|
|
15
25
|
{ commandId: 'menu:pull', label: 'menus', supportsYes: true },
|
|
16
26
|
{ commandId: 'option:pull', label: 'options', supportsYes: true },
|
|
17
|
-
{ commandId: 'snippet:pull', label: 'snippets', supportsYes: true },
|
|
27
|
+
{ commandId: 'snippet:pull', label: 'snippets', needsPlugin: 'snippets', supportsYes: true },
|
|
18
28
|
];
|
|
19
29
|
export default class Pull extends LoopressCommand {
|
|
20
|
-
static description = 'Pull all content, plugins, composer dependencies, ACF, API routes, hooks, forms,
|
|
30
|
+
static description = 'Pull all content, plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, menus, options, and snippets, from WordPress';
|
|
21
31
|
static enableJsonFlag = true;
|
|
22
32
|
static examples = ['$ lps pull', '$ lps pull --env staging', '$ lps pull --dry-run'];
|
|
23
33
|
static flags = {
|
|
@@ -27,6 +37,7 @@ export default class Pull extends LoopressCommand {
|
|
|
27
37
|
failedCount = 0;
|
|
28
38
|
async run() {
|
|
29
39
|
const results = [];
|
|
40
|
+
this.warnRemovedSeoModule();
|
|
30
41
|
for (const target of PULL_TARGETS) {
|
|
31
42
|
this.log(`\n→ Pulling ${target.label}...`);
|
|
32
43
|
try {
|
|
@@ -35,8 +46,19 @@ export default class Pull extends LoopressCommand {
|
|
|
35
46
|
results.push({ label: target.label, status: 'pulled' });
|
|
36
47
|
}
|
|
37
48
|
catch (error) {
|
|
38
|
-
this.failedCount++;
|
|
39
49
|
const { message } = error;
|
|
50
|
+
// A site without ACF (or a form or snippet plugin) is the common case, not a failure,
|
|
51
|
+
// as long as the project doesn't use that resource: a vanilla site must pull, and a
|
|
52
|
+
// promote must not stop on it. A project that does use it still fails.
|
|
53
|
+
if (target.needsPlugin && PLUGIN_MISSING.test(message) && !this.usesResource(target.needsPlugin)) {
|
|
54
|
+
// oclif's catch already set exitCode to 1 for the sub-command; a real failure still
|
|
55
|
+
// exits non-zero through the this.error() below.
|
|
56
|
+
process.exitCode = undefined;
|
|
57
|
+
this.log(`→ ${target.label}: not configured, skipped`);
|
|
58
|
+
results.push({ label: target.label, status: 'skipped' });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
this.failedCount++;
|
|
40
62
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
41
63
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
42
64
|
}
|
|
@@ -60,4 +82,7 @@ export default class Pull extends LoopressCommand {
|
|
|
60
82
|
argv.push('--yes');
|
|
61
83
|
return argv;
|
|
62
84
|
}
|
|
85
|
+
usesResource(kind) {
|
|
86
|
+
return existsSync(resolve(resolveResourceDir(kind, this.localConfig)));
|
|
87
|
+
}
|
|
63
88
|
}
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LoopressCommand } from '../lib/base.js';
|
|
|
2
2
|
type PushTargetResult = {
|
|
3
3
|
error?: string;
|
|
4
4
|
label: string;
|
|
5
|
-
status: 'failed' | 'pushed';
|
|
5
|
+
status: 'failed' | 'pushed' | 'skipped';
|
|
6
6
|
};
|
|
7
7
|
type PushResult = {
|
|
8
8
|
results: PushTargetResult[];
|
|
@@ -12,13 +12,15 @@ export default class Push extends LoopressCommand {
|
|
|
12
12
|
static enableJsonFlag: boolean;
|
|
13
13
|
static examples: string[];
|
|
14
14
|
static flags: {
|
|
15
|
+
overwrite: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
18
|
};
|
|
18
|
-
private failedCount;
|
|
19
19
|
run(): Promise<PushResult>;
|
|
20
20
|
private buildArgv;
|
|
21
21
|
private composerDeclaresPlugins;
|
|
22
|
+
private composerJsonPath;
|
|
22
23
|
private guardProductionPush;
|
|
24
|
+
private skippedTargets;
|
|
23
25
|
}
|
|
24
26
|
export {};
|