@loopress/cli 0.32.1 → 0.33.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 +511 -154
- package/dist/commands/acf/list.d.ts +1 -1
- package/dist/commands/acf/list.js +3 -4
- package/dist/commands/acf/pull.d.ts +1 -1
- package/dist/commands/acf/pull.js +9 -6
- package/dist/commands/acf/push.d.ts +1 -1
- package/dist/commands/acf/push.js +5 -4
- package/dist/commands/api/publish.js +2 -1
- package/dist/commands/app/pull.js +2 -1
- package/dist/commands/app/push.js +5 -4
- package/dist/commands/app/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/app/{remove.js → rm.js} +2 -1
- package/dist/commands/block/diff.d.ts +2 -0
- package/dist/commands/block/diff.js +5 -0
- package/dist/commands/block/list.d.ts +8 -0
- package/dist/commands/block/list.js +23 -0
- package/dist/commands/block/pull.d.ts +21 -0
- package/dist/commands/block/pull.js +67 -0
- package/dist/commands/block/push.d.ts +20 -0
- package/dist/commands/block/push.js +48 -0
- package/dist/commands/block/rm.d.ts +23 -0
- package/dist/commands/block/rm.js +69 -0
- package/dist/commands/block/rollback.d.ts +2 -0
- package/dist/commands/block/rollback.js +5 -0
- package/dist/commands/composer/init.js +1 -0
- package/dist/commands/composer/push.js +5 -4
- package/dist/commands/content/copy.d.ts +3 -0
- package/dist/commands/content/copy.js +79 -2
- package/dist/commands/dev.js +4 -3
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +33 -3
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +37 -23
- package/dist/commands/form/pull.js +7 -5
- package/dist/commands/form/push.js +4 -3
- package/dist/commands/init.js +4 -3
- package/dist/commands/menu/push.js +8 -8
- package/dist/commands/option/list.js +5 -2
- package/dist/commands/option/push.js +2 -1
- package/dist/commands/option/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/option/{remove.js → rm.js} +2 -1
- package/dist/commands/page/push.js +2 -1
- package/dist/commands/plugin/audit.d.ts +4 -4
- package/dist/commands/plugin/audit.js +35 -15
- package/dist/commands/plugin/pull.js +3 -2
- package/dist/commands/plugin/push.js +2 -2
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -6
- package/dist/commands/project/pull.js +3 -1
- package/dist/commands/project/push.js +6 -4
- package/dist/commands/project/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/project/{remove.js → rm.js} +3 -2
- package/dist/commands/project/switch.js +6 -4
- package/dist/commands/promote.js +7 -2
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +42 -18
- package/dist/commands/push.d.ts +2 -1
- package/dist/commands/push.js +69 -25
- package/dist/commands/snippet/publish.js +9 -5
- package/dist/commands/snippet/push.d.ts +1 -1
- package/dist/commands/snippet/push.js +15 -12
- package/dist/commands/status.js +1 -0
- package/dist/commands/theme/activate.d.ts +21 -0
- package/dist/commands/theme/activate.js +44 -0
- package/dist/commands/theme/pull.js +2 -1
- package/dist/commands/theme/push.js +7 -3
- package/dist/commands/theme/style/push.js +7 -2
- package/dist/commands/theme/template/push.js +25 -6
- package/dist/config/auth.manager.d.ts +10 -7
- package/dist/config/auth.manager.js +5 -1
- package/dist/config/json-file.d.ts +1 -1
- package/dist/config/project-config.manager.d.ts +2 -2
- package/dist/config/project-config.manager.js +13 -21
- package/dist/lib/api-client.js +16 -21
- package/dist/lib/app-build.js +6 -2
- package/dist/lib/app-manifest.js +6 -6
- package/dist/lib/base.d.ts +2 -1
- package/dist/lib/base.js +10 -8
- package/dist/lib/bootstrap-full-install.js +8 -6
- package/dist/lib/capabilities.d.ts +44 -0
- package/dist/lib/capabilities.js +85 -0
- package/dist/lib/declared-type-command.js +4 -2
- package/dist/lib/dev-watch.js +5 -5
- package/dist/lib/diff-command.d.ts +2 -3
- package/dist/lib/diff-command.js +9 -7
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +37 -42
- package/dist/lib/local-callback-server.js +7 -1
- package/dist/lib/narrow.d.ts +5 -0
- package/dist/lib/narrow.js +26 -0
- package/dist/lib/php-files-command.js +8 -5
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +1 -1
- package/dist/lib/push-command.js +19 -7
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.d.ts +1 -1
- package/dist/lib/resource-state.js +58 -19
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +3 -2
- package/dist/lib/rollback-materialize.js +28 -8
- package/dist/lib/rotate-app-password.js +1 -0
- package/dist/lib/sentry.js +2 -2
- package/dist/lib/snapshot-store.d.ts +10 -7
- package/dist/lib/snapshot-store.js +21 -5
- package/dist/lib/spinner.js +4 -0
- package/dist/lib/validate-local.js +35 -20
- package/dist/lib/wp-client.d.ts +6 -1
- package/dist/lib/wp-client.js +54 -69
- package/dist/lib/wp-site-diagnostic.js +3 -5
- package/dist/types/global-config.generated.d.ts +1 -1
- package/dist/types/plugin.d.ts +7 -5
- package/dist/types/plugin.js +6 -1
- package/dist/types/project-config.generated.d.ts +9 -1
- package/dist/types/snippet.d.ts +2 -2
- package/dist/types/snippet.js +1 -1
- package/dist/utils/acf-format.d.ts +1 -0
- package/dist/utils/acf-format.js +11 -3
- package/dist/utils/block-format.d.ts +27 -0
- package/dist/utils/block-format.js +93 -0
- package/dist/utils/composer.d.ts +7 -15
- package/dist/utils/composer.js +43 -16
- package/dist/utils/declared-type-format.js +26 -19
- package/dist/utils/form-format.js +2 -2
- package/dist/utils/loopress-config.js +9 -1
- package/dist/utils/menu-format.d.ts +3 -1
- package/dist/utils/menu-format.js +5 -0
- package/dist/utils/option-format.d.ts +2 -2
- package/dist/utils/option-format.js +15 -25
- package/dist/utils/page-format.js +7 -6
- package/dist/utils/php-top-level.js +15 -4
- package/dist/utils/plugin-sync.d.ts +7 -7
- package/dist/utils/plugin-sync.js +11 -11
- package/dist/utils/plugins.js +9 -6
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +9 -1
- package/dist/utils/snippet-format.d.ts +1 -1
- package/dist/utils/snippet-format.js +5 -3
- package/dist/utils/template-format.js +2 -2
- package/dist/utils/theme-css.js +14 -6
- package/dist/utils/theme-styles-format.d.ts +3 -0
- package/dist/utils/theme-styles-format.js +44 -6
- package/dist/utils/version.js +1 -1
- package/oclif.manifest.json +1588 -1043
- package/package.json +42 -17
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +11 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { errorMessage, isRecord } from '../../lib/narrow.js';
|
|
3
4
|
import { withSpinner } from '../../lib/spinner.js';
|
|
4
5
|
import { pinVersion } from '../../types/plugin.js';
|
|
5
6
|
import { compareVersions } from '../../utils/version.js';
|
|
@@ -19,8 +20,8 @@ export default class Audit extends LoopressCommand {
|
|
|
19
20
|
}
|
|
20
21
|
const advisories = [];
|
|
21
22
|
const health = [];
|
|
22
|
-
for (const slug of
|
|
23
|
-
const found = await withSpinner(`Auditing ${slug}`, async () => this.auditOne(slug, pinVersion(
|
|
23
|
+
for (const [slug, pin] of Object.entries(manifest)) {
|
|
24
|
+
const found = await withSpinner(`Auditing ${slug}`, async () => this.auditOne(slug, pinVersion(pin)));
|
|
24
25
|
advisories.push(...found.advisories);
|
|
25
26
|
health.push(...found.health);
|
|
26
27
|
}
|
|
@@ -59,11 +60,17 @@ export default class Audit extends LoopressCommand {
|
|
|
59
60
|
catch (error) {
|
|
60
61
|
// Never swallow this into an empty list: a clean audit must mean "no advisories found",
|
|
61
62
|
// not "the advisory service was unreachable". Fail the command instead.
|
|
62
|
-
throw new Error(`Could not reach the vulnerability database (wpvulnerability.net) for "${slug}": ${error
|
|
63
|
+
throw new Error(`Could not reach the vulnerability database (wpvulnerability.net) for "${slug}": ${errorMessage(error)}. ` +
|
|
63
64
|
'Audit aborted so a service outage is not reported as a clean result.', { cause: error });
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// A 200 with the wrong shape (degraded service) must not read as "no advisories" either.
|
|
67
|
+
const list = body?.data?.vulnerability;
|
|
68
|
+
if (!Array.isArray(list) || list.some((raw) => !isRecord(raw))) {
|
|
69
|
+
throw new TypeError(`Unexpected answer from the vulnerability database (wpvulnerability.net) for "${slug}". ` +
|
|
70
|
+
'Audit aborted so a malformed response is not reported as a clean result.');
|
|
71
|
+
}
|
|
72
|
+
// Already all objects (checked above), the filter only carries that to the type.
|
|
73
|
+
return list.filter((raw) => isRecord(raw)).map((raw) => normalizeAdvisory(raw));
|
|
67
74
|
}
|
|
68
75
|
report(advisories, health, count) {
|
|
69
76
|
if (advisories.length === 0 && health.length === 0) {
|
|
@@ -93,26 +100,28 @@ function advisoryMeta(a) {
|
|
|
93
100
|
return parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
94
101
|
}
|
|
95
102
|
function normalizeAdvisory(raw) {
|
|
96
|
-
const cvss = (raw.cvss
|
|
97
|
-
const
|
|
98
|
-
const cve = typeof raw.cve === 'string' ? raw.cve :
|
|
103
|
+
const cvss = isRecord(raw.cvss) ? raw.cvss : {};
|
|
104
|
+
const sourceIds = Array.isArray(raw.source) ? raw.source.map((s) => (isRecord(s) && typeof s.id === 'string' ? s.id : '')) : [];
|
|
105
|
+
const cve = typeof raw.cve === 'string' ? raw.cve : sourceIds.find((id) => id.startsWith('CVE-'));
|
|
99
106
|
const range = extractRange(raw);
|
|
100
107
|
return {
|
|
101
108
|
cve,
|
|
102
109
|
fixedIn: range.max?.op === 'lt' ? range.max.version : undefined,
|
|
103
110
|
range,
|
|
104
|
-
score: cvss.score,
|
|
105
|
-
severity: cvss.severity,
|
|
111
|
+
score: typeof cvss.score === 'number' ? cvss.score : undefined,
|
|
112
|
+
severity: typeof cvss.severity === 'string' ? cvss.severity : undefined,
|
|
106
113
|
title: typeof raw.name === 'string' ? decodeEntities(raw.name) : 'Unnamed advisory',
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
116
|
// wpvulnerability's `operator`: `max_version` with `max_operator` "lt" (fixed in that version),
|
|
110
117
|
// "le" (affected up to it, no fix yet) or "eq" (that version only), and an optional lower bound `min_version` with
|
|
111
118
|
// `min_operator` "gt" or "ge". No upper bound means every version from the lower one on.
|
|
119
|
+
function toBound(version, operator) {
|
|
120
|
+
return typeof version === 'string' && version !== '' && typeof operator === 'string' ? { op: operator, version } : undefined;
|
|
121
|
+
}
|
|
112
122
|
function extractRange(raw) {
|
|
113
|
-
const op = (raw.operator
|
|
114
|
-
|
|
115
|
-
return { max: bound(op.max_version, op.max_operator), min: bound(op.min_version, op.min_operator) };
|
|
123
|
+
const op = isRecord(raw.operator) ? raw.operator : {};
|
|
124
|
+
return { max: toBound(op.max_version, op.max_operator), min: toBound(op.min_version, op.min_operator) };
|
|
116
125
|
}
|
|
117
126
|
// A bound we can't evaluate (odd version strings, unknown operator) counts as affected: a false
|
|
118
127
|
// positive is safer than hiding an advisory.
|
|
@@ -121,7 +130,8 @@ function affects(version, range) {
|
|
|
121
130
|
if (!bound)
|
|
122
131
|
return true;
|
|
123
132
|
const order = compareVersions(version, bound.version);
|
|
124
|
-
|
|
133
|
+
const check = Object.hasOwn(ok, bound.op) ? ok[bound.op] : undefined;
|
|
134
|
+
return order === null || !check || check(order);
|
|
125
135
|
};
|
|
126
136
|
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 }));
|
|
127
137
|
}
|
|
@@ -147,9 +157,19 @@ function describeHealth(slug, pinned, info) {
|
|
|
147
157
|
}
|
|
148
158
|
const lastUpdated = typeof info.last_updated === 'string' ? info.last_updated : null;
|
|
149
159
|
if (lastUpdated) {
|
|
150
|
-
const years = (Date.now() -
|
|
160
|
+
const years = (Date.now() - parseWordPressDate(lastUpdated)) / (365 * 24 * 3600 * 1000);
|
|
151
161
|
if (years > 2)
|
|
152
162
|
notes.push({ message: `last updated ${lastUpdated.slice(0, 10)} (possibly abandoned)`, slug });
|
|
153
163
|
}
|
|
154
164
|
return notes;
|
|
155
165
|
}
|
|
166
|
+
// WordPress.org's plugins API returns `last_updated` as "2015-03-04 10:00am GMT", which
|
|
167
|
+
// Date.parse() reads as NaN. Rewrite that form to ISO, and leave anything else to Date.parse().
|
|
168
|
+
function parseWordPressDate(value) {
|
|
169
|
+
const match = /^(\d{4}-\d{2}-\d{2}) (\d{1,2}):(\d{2})(am|pm) GMT$/i.exec(value);
|
|
170
|
+
if (!match)
|
|
171
|
+
return Date.parse(value);
|
|
172
|
+
const [, day, hour, minute, half] = match;
|
|
173
|
+
const hours = (Number(hour) % 12) + (half?.toLowerCase() === 'pm' ? 12 : 0);
|
|
174
|
+
return Date.parse(`${day}T${String(hours).padStart(2, '0')}:${minute}:00Z`);
|
|
175
|
+
}
|
|
@@ -5,7 +5,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { LoopressCommand } from '../../lib/base.js';
|
|
7
7
|
import { isInteractive } from '../../lib/interactive.js';
|
|
8
|
-
import {
|
|
8
|
+
import {} from '../../types/plugin.js';
|
|
9
|
+
import { getComposerManagedSlugs, NOT_ON_WORDPRESS_ORG, parseComposerJson, pullIntoComposerJson, } from '../../utils/composer.js';
|
|
9
10
|
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
10
11
|
import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
11
12
|
export default class Pull extends LoopressCommand {
|
|
@@ -29,7 +30,7 @@ export default class Pull extends LoopressCommand {
|
|
|
29
30
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
30
31
|
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
31
32
|
const composerJson = existsSync(composerJsonPath)
|
|
32
|
-
?
|
|
33
|
+
? parseComposerJson(await readFile(composerJsonPath, 'utf8'))
|
|
33
34
|
: undefined;
|
|
34
35
|
const declared = new Set(composerJson ? getComposerManagedSlugs(composerJson) : Object.keys(this.localConfig.plugins ?? {}));
|
|
35
36
|
const all = parseInstalledPlugins(raw);
|
|
@@ -6,7 +6,7 @@ import { confirmUninstall } from '../../lib/interactive.js';
|
|
|
6
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
7
|
import { isLocalEnvironment, isNotFoundError } from '../../lib/wp-client.js';
|
|
8
8
|
import { pinActive, pinVersion, pinVersions, } from '../../types/plugin.js';
|
|
9
|
-
import { requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
9
|
+
import { parseComposerJson, requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
10
10
|
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
11
11
|
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
12
12
|
import { isExactVersion } from '../../utils/version.js';
|
|
@@ -177,7 +177,7 @@ export default class Push extends PushCommand {
|
|
|
177
177
|
async loadSource(activate) {
|
|
178
178
|
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
179
179
|
if (existsSync(composerJsonPath)) {
|
|
180
|
-
const composerJson =
|
|
180
|
+
const composerJson = parseComposerJson(await readFile(composerJsonPath, 'utf8'));
|
|
181
181
|
const includeDev = isLocalEnvironment(this.siteConfig.name);
|
|
182
182
|
const manifest = wpackagistRequire(composerJson, 'plugin', includeDev);
|
|
183
183
|
if (Object.keys(manifest).length === 0) {
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
4
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
5
|
+
import {} from '../../types/plugin.js';
|
|
5
6
|
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
6
7
|
export default class Status extends LoopressCommand {
|
|
7
8
|
static description = 'Compare the plugins on WordPress against loopress.json and report drift (missing, wrong version, inactive, untracked)';
|
|
@@ -64,7 +65,8 @@ export default class Status extends LoopressCommand {
|
|
|
64
65
|
drift,
|
|
65
66
|
inactive,
|
|
66
67
|
missing,
|
|
67
|
-
|
|
68
|
+
// Drift exits with code 1 above, so only the untracked-active report gets here.
|
|
69
|
+
status: 'in-sync',
|
|
68
70
|
unexpectedlyActive,
|
|
69
71
|
untrackedActive,
|
|
70
72
|
wrongVersion,
|
|
@@ -3,12 +3,14 @@ import { Command } from '@oclif/core';
|
|
|
3
3
|
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
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
6
7
|
import { isLoopressFullActive } from '../../lib/plugin-detection.js';
|
|
7
8
|
import { withSpinner } from '../../lib/spinner.js';
|
|
8
9
|
import { sweepTempAdmins } from '../../lib/temp-admin.js';
|
|
9
10
|
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
10
11
|
import { isLocalEnvironment, WpClient } from '../../lib/wp-client.js';
|
|
11
12
|
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
13
|
+
import {} from '../../types/config.js';
|
|
12
14
|
import { pluralize } from '../../utils/pluralize.js';
|
|
13
15
|
const NEW_PROJECT = '__new__';
|
|
14
16
|
const AUTH_BROWSER = 'browser';
|
|
@@ -121,7 +123,7 @@ export default class Config extends Command {
|
|
|
121
123
|
}));
|
|
122
124
|
}
|
|
123
125
|
catch (error) {
|
|
124
|
-
this.warn(error
|
|
126
|
+
this.warn(errorMessage(error));
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
async promptManualCredentials() {
|
|
@@ -157,7 +159,7 @@ export default class Config extends Command {
|
|
|
157
159
|
return { appPassword: password, user: userLogin };
|
|
158
160
|
}
|
|
159
161
|
catch (error) {
|
|
160
|
-
this.warn(`${error
|
|
162
|
+
this.warn(`${errorMessage(error)}\nFalling back to manual credential entry.`);
|
|
161
163
|
return this.promptManualCredentials();
|
|
162
164
|
}
|
|
163
165
|
}
|
|
@@ -171,10 +173,9 @@ export default class Config extends Command {
|
|
|
171
173
|
],
|
|
172
174
|
message: 'Project',
|
|
173
175
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const project = projects.find((p) => p.id === choice);
|
|
177
|
+
if (project)
|
|
176
178
|
return { projectId: project.id, projectName: project.name };
|
|
177
|
-
}
|
|
178
179
|
}
|
|
179
180
|
const existingNames = new Set(projects.map((project) => project.name.trim().toLowerCase()));
|
|
180
181
|
const projectName = await input({
|
|
@@ -197,7 +198,7 @@ export default class Config extends Command {
|
|
|
197
198
|
this.log(`✓ Removed ${pluralize(removed.length, 'leftover temporary admin account')}: ${removed.join(', ')}`);
|
|
198
199
|
}
|
|
199
200
|
catch (error) {
|
|
200
|
-
this.warn(error
|
|
201
|
+
this.warn(errorMessage(error));
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -3,7 +3,9 @@ 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 { errorMessage } from '../../lib/narrow.js';
|
|
6
7
|
import { withoutSpinner } from '../../lib/spinner.js';
|
|
8
|
+
import {} from '../../types/config.js';
|
|
7
9
|
import { pluralize } from '../../utils/pluralize.js';
|
|
8
10
|
export default class Pull extends Command {
|
|
9
11
|
static description = 'Pull projects and environments from your Loopress account that are not configured locally yet';
|
|
@@ -42,7 +44,7 @@ export default class Pull extends Command {
|
|
|
42
44
|
return await api.get('projects');
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
|
-
this.error(`Could not fetch projects from the API: ${error
|
|
47
|
+
this.error(`Could not fetch projects from the API: ${errorMessage(error)}`);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
// Reuse a local project already linked to this API project instead of always minting a new
|
|
@@ -6,7 +6,9 @@ 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 { errorMessage } from '../../lib/narrow.js';
|
|
9
10
|
import { withoutSpinner } from '../../lib/spinner.js';
|
|
11
|
+
import {} from '../../types/config.js';
|
|
10
12
|
import { pluralize } from '../../utils/pluralize.js';
|
|
11
13
|
import { toSlug } from '../../utils/to-slug.js';
|
|
12
14
|
export default class Push extends Command {
|
|
@@ -57,7 +59,7 @@ export default class Push extends Command {
|
|
|
57
59
|
task.output = envPlan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
58
60
|
}
|
|
59
61
|
catch (error) {
|
|
60
|
-
const message = `Failed to push "${envPlan.env.name}": ${error
|
|
62
|
+
const message = `Failed to push "${envPlan.env.name}": ${errorMessage(error)}`;
|
|
61
63
|
if (task)
|
|
62
64
|
task.output = message;
|
|
63
65
|
throw error;
|
|
@@ -94,7 +96,7 @@ export default class Push extends Command {
|
|
|
94
96
|
task.output = plan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
95
97
|
}
|
|
96
98
|
catch (error) {
|
|
97
|
-
const message = `Failed to push project "${plan.project.name}": ${error
|
|
99
|
+
const message = `Failed to push project "${plan.project.name}": ${errorMessage(error)}`;
|
|
98
100
|
if (task)
|
|
99
101
|
task.output = message;
|
|
100
102
|
throw error;
|
|
@@ -138,7 +140,7 @@ export default class Push extends Command {
|
|
|
138
140
|
return await api.get('projects');
|
|
139
141
|
}
|
|
140
142
|
catch (error) {
|
|
141
|
-
this.warn(`Could not fetch existing projects from the API, will create everything as new: ${error
|
|
143
|
+
this.warn(`Could not fetch existing projects from the API, will create everything as new: ${errorMessage(error)}`);
|
|
142
144
|
return [];
|
|
143
145
|
}
|
|
144
146
|
}
|
|
@@ -203,7 +205,7 @@ export default class Push extends Command {
|
|
|
203
205
|
await this.pushCredentials(api, plan.apiProjectId, envPlan.apiEnvironmentId, envPlan.env);
|
|
204
206
|
}
|
|
205
207
|
catch (error) {
|
|
206
|
-
this.warn(`Failed to push "${plan.project.name}/${envPlan.env.name}": ${error
|
|
208
|
+
this.warn(`Failed to push "${plan.project.name}/${envPlan.env.name}": ${errorMessage(error)}`);
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
}
|
|
@@ -2,8 +2,9 @@ import { checkbox } from '@inquirer/prompts';
|
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
4
|
export default class Remove extends Command {
|
|
5
|
+
static aliases = ['project:remove'];
|
|
5
6
|
static description = 'Remove one or more WordPress projects or environments';
|
|
6
|
-
static examples = ['$ lps project
|
|
7
|
+
static examples = ['$ lps project rm'];
|
|
7
8
|
async run() {
|
|
8
9
|
await this.parse(Remove);
|
|
9
10
|
const projects = configManager.listProjects();
|
|
@@ -38,7 +39,7 @@ export default class Remove extends Command {
|
|
|
38
39
|
this.log('Nothing removed.');
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
const selected = chosen.
|
|
42
|
+
const selected = chosen.flatMap((index) => targets[Number(index)] ?? []);
|
|
42
43
|
const projectsToRemove = new Map(selected.filter((target) => target.kind === 'project').map((target) => [target.projectId, target.projectName]));
|
|
43
44
|
const envsToRemove = selected.filter((target) => target.kind === 'env' && !projectsToRemove.has(target.projectId));
|
|
44
45
|
for (const projectId of projectsToRemove.keys())
|
|
@@ -10,18 +10,20 @@ export default class Switch extends Command {
|
|
|
10
10
|
if (projects.length === 0) {
|
|
11
11
|
this.error('No projects configured. Run `lps project config` first.');
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const [only] = projects;
|
|
14
|
+
const currentId = projects.find((project) => project.isCurrent)?.id;
|
|
15
|
+
const projectId = only && projects.length === 1
|
|
16
|
+
? only.id
|
|
15
17
|
: await select({
|
|
16
18
|
choices: projects.map((project) => ({
|
|
17
19
|
name: `${project.name}${project.isCurrent ? ' [current]' : ''}`,
|
|
18
20
|
value: project.id,
|
|
19
21
|
})),
|
|
20
|
-
default:
|
|
22
|
+
...(currentId !== undefined && { default: currentId }),
|
|
21
23
|
message: 'Select project',
|
|
22
24
|
});
|
|
23
25
|
configManager.setCurrent(projectId);
|
|
24
26
|
const project = projects.find((candidate) => candidate.id === projectId);
|
|
25
|
-
this.log(`✓ Switched to "${project
|
|
27
|
+
this.log(`✓ Switched to "${project?.name ?? projectId}"`);
|
|
26
28
|
}
|
|
27
29
|
}
|
package/dist/commands/promote.js
CHANGED
|
@@ -8,7 +8,9 @@ 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 { errorMessage } from '../lib/narrow.js';
|
|
11
12
|
import { PushCommand } from '../lib/push-command.js';
|
|
13
|
+
import {} from '../types/config.js';
|
|
12
14
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
13
15
|
import { resolveResourceDir, RESOURCE_DIR_DEFAULTS } from '../utils/resource-dirs.js';
|
|
14
16
|
export default class Promote extends Command {
|
|
@@ -56,7 +58,7 @@ export default class Promote extends Command {
|
|
|
56
58
|
// A partial pull must never be pushed onward: stop before touching <to>.
|
|
57
59
|
// "untouched" is only true of <to>: a real (non dry-run) pull has already rewritten the files.
|
|
58
60
|
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
|
|
61
|
+
this.error(`Pull from ${from.name} failed, nothing was pushed to ${to.name}.${filesNote} ${errorMessage(error)}`);
|
|
60
62
|
}
|
|
61
63
|
this.log(`\n=== Pushing to ${to.name} (${to.url}) ===`);
|
|
62
64
|
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand('push', [...this.delegateArgv(to.name, dryRun), ...(overwrite ? ['--overwrite'] : [])]));
|
|
@@ -78,7 +80,10 @@ export default class Promote extends Command {
|
|
|
78
80
|
// resource dir would land outside the copy, in the user's real files, during a dry run.
|
|
79
81
|
async assertDirsInsideProject(projectDir) {
|
|
80
82
|
const localConfig = await readLocalConfig();
|
|
81
|
-
|
|
83
|
+
// Object.keys() is typed string[] even on an `as const` object: these are its own keys.
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
85
|
+
const kinds = Object.keys(RESOURCE_DIR_DEFAULTS);
|
|
86
|
+
const dirs = [localConfig.rootDir ?? '.', ...kinds.map((kind) => resolveResourceDir(kind, localConfig))];
|
|
82
87
|
const outside = dirs.find((dir) => {
|
|
83
88
|
const rel = relative(projectDir, resolve(projectDir, dir));
|
|
84
89
|
return rel.startsWith('..') || isAbsolute(rel);
|
package/dist/commands/pull.d.ts
CHANGED
package/dist/commands/pull.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LoopressCommand } from '../lib/base.js';
|
|
2
|
+
import { missingIntegration, missingModule } from '../lib/capabilities.js';
|
|
2
3
|
import { stdoutToStderr } from '../lib/json-delegation.js';
|
|
3
|
-
import {
|
|
4
|
+
import { errorMessage } from '../lib/narrow.js';
|
|
4
5
|
import { pluralize } from '../utils/pluralize.js';
|
|
5
6
|
import { projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
|
|
6
7
|
import { globalStylesSkipNote } from '../utils/theme-styles-format.js';
|
|
@@ -15,10 +16,11 @@ const PULL_TARGETS = [
|
|
|
15
16
|
{ commandId: 'meta:pull', label: 'post meta', supportsYes: true },
|
|
16
17
|
{ commandId: 'role:pull', label: 'roles', supportsYes: true },
|
|
17
18
|
{ commandId: 'acf:pull', label: 'ACF', needsPlugin: 'acf', supportsYes: true },
|
|
18
|
-
{ commandId: 'api:pull', label: 'API routes', supportsYes: true },
|
|
19
|
-
{ commandId: 'hook:pull', label: 'hooks', supportsYes: true },
|
|
19
|
+
{ commandId: 'api:pull', label: 'API routes', needsPlugin: 'api', supportsYes: true },
|
|
20
|
+
{ commandId: 'hook:pull', label: 'hooks', needsPlugin: 'hooks', supportsYes: true },
|
|
21
|
+
{ commandId: 'block:pull', label: 'dynamic blocks', needsPlugin: 'block', supportsYes: true },
|
|
20
22
|
{ commandId: 'form:pull', label: 'forms', needsPlugin: 'form', supportsYes: true },
|
|
21
|
-
{ commandId: 'pattern:pull', label: 'patterns', supportsYes: true },
|
|
23
|
+
{ commandId: 'pattern:pull', label: 'patterns', needsPlugin: 'pattern', supportsYes: true },
|
|
22
24
|
// Templates and parts have no pull. Global Styles are pulled only once the project tracks them,
|
|
23
25
|
// see globalStylesSkipNote.
|
|
24
26
|
{ commandId: 'theme:style:pull', label: 'Global Styles' },
|
|
@@ -26,8 +28,15 @@ const PULL_TARGETS = [
|
|
|
26
28
|
{ commandId: 'option:pull', label: 'options', supportsYes: true },
|
|
27
29
|
{ commandId: 'snippet:pull', label: 'snippets', needsPlugin: 'snippets', supportsYes: true },
|
|
28
30
|
];
|
|
31
|
+
// Why this site can't give the target. Composer has no directory to tell whether the project
|
|
32
|
+
// uses it: never pulled from Light, which has nothing to give.
|
|
33
|
+
function missingFor(target, capabilities) {
|
|
34
|
+
if (target.commandId === 'composer:pull')
|
|
35
|
+
return missingModule('dependencies', 'Composer files', capabilities);
|
|
36
|
+
return target.needsPlugin === undefined ? undefined : missingIntegration(target.needsPlugin, capabilities);
|
|
37
|
+
}
|
|
29
38
|
export default class Pull extends LoopressCommand {
|
|
30
|
-
static description = 'Pull every tracked resource from WordPress into local files in one run: composer dependencies, plugins, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, Global Styles, menus, options, and snippets';
|
|
39
|
+
static description = 'Pull every tracked resource from WordPress into local files in one run: composer dependencies, plugins, post types, taxonomies, post meta, roles, ACF, API routes, hooks, dynamic blocks, forms, patterns, Global Styles, menus, options, and snippets';
|
|
31
40
|
static enableJsonFlag = true;
|
|
32
41
|
static examples = ['$ lps pull', '$ lps pull --env staging', '$ lps pull --dry-run'];
|
|
33
42
|
static flags = {
|
|
@@ -37,7 +46,7 @@ export default class Pull extends LoopressCommand {
|
|
|
37
46
|
failedCount = 0;
|
|
38
47
|
async run() {
|
|
39
48
|
const results = [];
|
|
40
|
-
this.
|
|
49
|
+
const capabilities = await this.capabilities();
|
|
41
50
|
for (const target of PULL_TARGETS) {
|
|
42
51
|
const skipNote = target.commandId === 'theme:style:pull' ? await this.globalStylesSkipNote() : undefined;
|
|
43
52
|
if (skipNote) {
|
|
@@ -45,6 +54,11 @@ export default class Pull extends LoopressCommand {
|
|
|
45
54
|
results.push({ label: target.label, status: 'skipped' });
|
|
46
55
|
continue;
|
|
47
56
|
}
|
|
57
|
+
const unavailable = this.unavailable(target, capabilities);
|
|
58
|
+
if (unavailable) {
|
|
59
|
+
results.push(unavailable);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
48
62
|
this.log(`\n→ Pulling ${target.label}...`);
|
|
49
63
|
try {
|
|
50
64
|
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand(target.commandId, this.buildArgv(target)));
|
|
@@ -52,18 +66,7 @@ export default class Pull extends LoopressCommand {
|
|
|
52
66
|
results.push({ label: target.label, status: 'pulled' });
|
|
53
67
|
}
|
|
54
68
|
catch (error) {
|
|
55
|
-
const
|
|
56
|
-
// A site without ACF (or a form or snippet plugin) is the common case, not a failure,
|
|
57
|
-
// as long as the project doesn't use that resource: a vanilla site must pull, and a
|
|
58
|
-
// promote must not stop on it. A project that does use it still fails.
|
|
59
|
-
if (target.needsPlugin && INTEGRATION_MISSING.test(message) && !projectUses(target.needsPlugin, this.localConfig)) {
|
|
60
|
-
// oclif's catch already set exitCode to 1 for the sub-command; a real failure still
|
|
61
|
-
// exits non-zero through the this.error() below.
|
|
62
|
-
process.exitCode = undefined;
|
|
63
|
-
this.log(`→ ${target.label}: not configured, skipped`);
|
|
64
|
-
results.push({ label: target.label, status: 'skipped' });
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
69
|
+
const message = errorMessage(error);
|
|
67
70
|
this.failedCount++;
|
|
68
71
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
69
72
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
@@ -97,4 +100,25 @@ export default class Pull extends LoopressCommand {
|
|
|
97
100
|
return undefined;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
103
|
+
// Composer has no directory: counted as unused, see missingFor.
|
|
104
|
+
projectUses(target) {
|
|
105
|
+
return target.needsPlugin !== undefined && projectUses(target.needsPlugin, this.localConfig);
|
|
106
|
+
}
|
|
107
|
+
// A site without ACF (or a form or snippet plugin) is the common case, not a failure, as
|
|
108
|
+
// long as the project doesn't use that resource: a vanilla site must pull, and a promote
|
|
109
|
+
// must not stop on it. A project that does use it still fails, without asking the site.
|
|
110
|
+
// Same for a Loopress Full feature on Light.
|
|
111
|
+
unavailable(target, capabilities) {
|
|
112
|
+
const missing = missingFor(target, capabilities);
|
|
113
|
+
if (!missing)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (!this.projectUses(target)) {
|
|
116
|
+
this.log(`\n→ ${target.label}: ${missing.needsFull ? missing.reason : 'not configured'}, skipped`);
|
|
117
|
+
return { label: target.label, status: 'skipped' };
|
|
118
|
+
}
|
|
119
|
+
this.failedCount++;
|
|
120
|
+
const error = `${missing.reason} on ${this.siteConfig.name}`;
|
|
121
|
+
this.log(`\n✗ ${target.label} failed: ${error}`);
|
|
122
|
+
return { error, label: target.label, status: 'failed' };
|
|
123
|
+
}
|
|
100
124
|
}
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -18,9 +18,10 @@ export default class Push extends LoopressCommand {
|
|
|
18
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
19
|
};
|
|
20
20
|
run(): Promise<PushResult>;
|
|
21
|
+
private applySiteSteps;
|
|
21
22
|
private buildArgv;
|
|
22
|
-
private composerDeclaresPlugins;
|
|
23
23
|
private composerJsonPath;
|
|
24
|
+
private declaredPlugins;
|
|
24
25
|
private globalStylesSkipNote;
|
|
25
26
|
private guardProductionPush;
|
|
26
27
|
private skippedTargets;
|