@loopress/cli 0.32.2 → 0.34.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 +549 -179
- 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 +7 -5
- 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 +53 -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.d.ts +2 -2
- package/dist/commands/dev.js +13 -18
- package/dist/commands/diff.d.ts +6 -3
- package/dist/commands/diff.js +63 -13
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +49 -25
- 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 +3 -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 +3 -1
- package/dist/commands/plugin/add.d.ts +1 -0
- package/dist/commands/plugin/add.js +50 -9
- 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 +41 -6
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -7
- 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.d.ts +1 -0
- package/dist/commands/promote.js +15 -6
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.js +67 -18
- package/dist/commands/push.d.ts +11 -1
- package/dist/commands/push.js +157 -48
- 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/diff.js +1 -1
- package/dist/commands/theme/style/push.js +8 -3
- package/dist/commands/theme/style/rollback.js +1 -1
- package/dist/commands/theme/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +66 -18
- package/dist/commands/validate.js +3 -0
- 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 +8 -1
- package/dist/lib/base.js +43 -9
- 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-plugins.d.ts +9 -0
- package/dist/lib/declared-plugins.js +21 -0
- package/dist/lib/declared-type-command.js +7 -2
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +13 -15
- package/dist/lib/diff-command.d.ts +19 -3
- package/dist/lib/diff-command.js +132 -14
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/environment-urls.d.ts +6 -0
- package/dist/lib/environment-urls.js +59 -0
- package/dist/lib/exclude.d.ts +16 -0
- package/dist/lib/exclude.js +102 -0
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +39 -43
- 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 +13 -8
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +5 -1
- package/dist/lib/push-command.js +48 -7
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +9 -4
- package/dist/lib/resource-selection.d.ts +9 -0
- package/dist/lib/resource-selection.js +49 -0
- package/dist/lib/resource-state.d.ts +2 -0
- package/dist/lib/resource-state.js +60 -14
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +4 -3
- package/dist/lib/rollback-materialize.js +29 -9
- 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.d.ts +1 -0
- package/dist/lib/validate-local.js +102 -28
- package/dist/lib/wp-client.d.ts +7 -1
- package/dist/lib/wp-client.js +60 -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 +24 -5
- package/dist/types/snippet.d.ts +2 -2
- package/dist/types/snippet.js +1 -1
- package/dist/utils/acf-format.js +6 -3
- package/dist/utils/block-format.d.ts +29 -0
- package/dist/utils/block-format.js +109 -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 +9 -7
- 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 +13 -5
- 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 +15 -5
- package/dist/utils/version.js +1 -1
- package/oclif.manifest.json +2661 -1235
- package/package.json +45 -20
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +32 -5
|
@@ -79,11 +79,14 @@ const NON_TTY_FALLBACK_COLUMNS = 300;
|
|
|
79
79
|
// column. See confidenceColor() above for SOURCE?/PLUGIN's shared sûr/pas sûr color coding.
|
|
80
80
|
function renderTable(rows, { includeCore }) {
|
|
81
81
|
const previousColumns = process.env.COLUMNS;
|
|
82
|
-
|
|
82
|
+
// Undefined on a non-TTY stdout, despite its `number` type.
|
|
83
|
+
const ttyColumns = process.stdout.columns > 0 ? process.stdout.columns : undefined;
|
|
84
|
+
if (!previousColumns && ttyColumns === undefined)
|
|
83
85
|
process.env.COLUMNS = String(NON_TTY_FALLBACK_COLUMNS);
|
|
84
86
|
// Mirrors tty-table's own getAvailableWidth(): its column-width percentages are computed against
|
|
85
87
|
// this same number, so fillPercent() above needs it too to reserve the right amount for AUTOLOAD/CORE.
|
|
86
|
-
const
|
|
88
|
+
const envColumns = Number(process.env.COLUMNS);
|
|
89
|
+
const width = (ttyColumns ?? (envColumns > 0 ? envColumns : NON_TTY_FALLBACK_COLUMNS)) - 2;
|
|
87
90
|
const header = [
|
|
88
91
|
{ align: 'left', headerAlign: 'left', value: 'NAME', width: columnWidth(COLUMN_WEIGHT.name, width, includeCore) },
|
|
89
92
|
{ align: 'left', headerAlign: 'left', value: 'AUTOLOAD' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { loadFiles } from '../../lib/load-files.js';
|
|
3
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
3
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
4
5
|
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
5
6
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
@@ -39,6 +40,7 @@ export default class Push extends PushCommand {
|
|
|
39
40
|
this.log(`Skipping ${pluralize(skipped.length, 'readonly option')}: ${skipped.map((option) => option.name).join(', ')}`);
|
|
40
41
|
}
|
|
41
42
|
this.log(`Found ${pluralize(writable.length, 'option')} to push`);
|
|
43
|
+
this.warnEnvironmentUrls(writable.map((option) => ({ id: `options/${option.name}`, value: option.value })));
|
|
42
44
|
const pushed = [];
|
|
43
45
|
await this.runPushTasks(writable, (option) => option.name, async (option, task) => {
|
|
44
46
|
await this.pushOption(option, task);
|
|
@@ -89,7 +91,7 @@ export default class Push extends PushCommand {
|
|
|
89
91
|
task.output = `Pushed: ${option.name}`;
|
|
90
92
|
}
|
|
91
93
|
catch (error) {
|
|
92
|
-
this.reportTaskFailure(`Failed to push ${option.name}: ${error
|
|
94
|
+
this.reportTaskFailure(`Failed to push ${option.name}: ${errorMessage(error)}`, error, task);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -8,13 +8,14 @@ import { isInteractive } from '../../lib/interactive.js';
|
|
|
8
8
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
9
9
|
import { optionEndpoint, optionFileName } from '../../utils/option-format.js';
|
|
10
10
|
export default class Remove extends LoopressCommand {
|
|
11
|
+
static aliases = ['option:remove'];
|
|
11
12
|
static args = {
|
|
12
13
|
name: Args.string({ description: 'Option name to stop tracking', required: true }),
|
|
13
14
|
};
|
|
14
15
|
static description = 'Stop tracking an option locally and delete it from WordPress. Local untracking always happens (git-reversible); ' +
|
|
15
16
|
'pass --local-only to skip the WordPress delete and only stop tracking.';
|
|
16
17
|
static enableJsonFlag = true;
|
|
17
|
-
static examples = ['$ lps option
|
|
18
|
+
static examples = ['$ lps option rm wpseo_titles', '$ lps option rm wpseo_titles --local-only'];
|
|
18
19
|
static flags = {
|
|
19
20
|
...LoopressCommand.dryRunFlag,
|
|
20
21
|
...LoopressCommand.yesFlag,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
2
|
import { Args } from '@oclif/core';
|
|
3
3
|
import { isInteractive } from '../../lib/interactive.js';
|
|
4
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
4
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
5
6
|
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
6
7
|
import { isConflictCode } from '../../lib/wp-client.js';
|
|
@@ -34,6 +35,7 @@ export default class Push extends PushCommand {
|
|
|
34
35
|
this.error(`No page "${args.slug}" in ${path} (expected ${args.slug}.html).`);
|
|
35
36
|
}
|
|
36
37
|
this.log(`Found ${pluralize(selected.length, 'page')} to push`);
|
|
38
|
+
this.warnEnvironmentUrls(selected.map((page) => ({ id: `pages/${page.slug}`, value: page.html })));
|
|
37
39
|
const provider = getResourceStateProvider('page');
|
|
38
40
|
const beforeState = await this.captureBeforePushState(provider, path, args.slug === undefined ? undefined : (id) => id === args.slug);
|
|
39
41
|
const pushed = [];
|
|
@@ -90,7 +92,7 @@ export default class Push extends PushCommand {
|
|
|
90
92
|
task?.skip?.(`${page.slug} already exists on the site, not created by Loopress`);
|
|
91
93
|
return false;
|
|
92
94
|
}
|
|
93
|
-
this.reportTaskFailure(`Failed to push ${page.slug}: ${error
|
|
95
|
+
this.reportTaskFailure(`Failed to push ${page.slug}: ${errorMessage(error)}`, error, task);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import got from 'got';
|
|
2
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
-
import { pinVersion } from '../../types/plugin.js';
|
|
4
|
+
import { pinActive, pinVersion } from '../../types/plugin.js';
|
|
4
5
|
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
5
6
|
import { isExactVersion } from '../../utils/version.js';
|
|
6
7
|
export default class Add extends LoopressCommand {
|
|
@@ -26,22 +27,62 @@ export default class Add extends LoopressCommand {
|
|
|
26
27
|
this.error(`--version must be an exact version like 9.4.2, not a Composer constraint. Got "${version}".`);
|
|
27
28
|
}
|
|
28
29
|
const existing = this.localConfig.plugins ?? {};
|
|
30
|
+
const required = await requiredPlugins(slug);
|
|
31
|
+
const dependencies = required.filter((dep) => !Object.hasOwn(existing, dep));
|
|
32
|
+
// An inactive pin keeps its dependency off, so push could never activate `slug`.
|
|
33
|
+
const inactive = required.filter((dep) => Object.hasOwn(existing, dep) && !pinActive(existing[dep]));
|
|
34
|
+
if (inactive.length > 0) {
|
|
35
|
+
this.warn(`${slug} requires ${inactive.join(', ')}, pinned inactive in loopress.json. Remove \`"active": false\` there, or push cannot activate ${slug}.`);
|
|
36
|
+
}
|
|
29
37
|
const current = existing[slug];
|
|
30
|
-
if (current !== undefined && pinVersion(current) === version) {
|
|
38
|
+
if (current !== undefined && pinVersion(current) === version && dependencies.length === 0) {
|
|
31
39
|
this.log(`${slug} is already pinned to ${version} in loopress.json, nothing to do.`);
|
|
32
|
-
return { slug, status: 'unchanged', version };
|
|
40
|
+
return { dependencies, slug, status: 'unchanged', version };
|
|
33
41
|
}
|
|
34
42
|
const isUpdated = current !== undefined;
|
|
43
|
+
const requires = dependencies.length > 0 ? `, with required ${dependencies.join(', ')} (latest)` : '';
|
|
35
44
|
if (this.dryRun) {
|
|
36
|
-
this.log(`[dry-run] Would ${isUpdated ? 'update' : 'add'} ${slug} (${version}) in loopress.json`);
|
|
37
|
-
return { slug, status: 'dry-run', version };
|
|
45
|
+
this.log(`[dry-run] Would ${isUpdated ? 'update' : 'add'} ${slug} (${version})${requires} in loopress.json`);
|
|
46
|
+
return { dependencies, slug, status: 'dry-run', version };
|
|
38
47
|
}
|
|
39
48
|
await writeLocalConfig({
|
|
40
49
|
...this.localConfig,
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
plugins: {
|
|
51
|
+
...existing,
|
|
52
|
+
...Object.fromEntries(dependencies.map((dep) => [dep, 'latest'])),
|
|
53
|
+
// Re-pinning a plugin recorded as inactive keeps it inactive.
|
|
54
|
+
[slug]: typeof current === 'object' ? { ...current, version } : version,
|
|
55
|
+
},
|
|
43
56
|
});
|
|
44
|
-
this.log(`${isUpdated ? 'Updated' : 'Added'} ${slug} (${version}). Run \`lps plugin push\` to apply.`);
|
|
45
|
-
return { slug, status: isUpdated ? 'updated' : 'added', version };
|
|
57
|
+
this.log(`${isUpdated ? 'Updated' : 'Added'} ${slug} (${version})${requires}. Run \`lps plugin push\` to apply.`);
|
|
58
|
+
return { dependencies, slug, status: isUpdated ? 'updated' : 'added', version };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Every plugin `slug` needs active first, transitively, from the `Requires Plugins` header
|
|
62
|
+
// WordPress.org exposes as `requires_plugins`. Not on WordPress.org or unreachable: none known,
|
|
63
|
+
// and `lps plugin push` still names a missing one when WordPress refuses the activation.
|
|
64
|
+
async function requiredPlugins(slug) {
|
|
65
|
+
const found = [];
|
|
66
|
+
const queue = [slug];
|
|
67
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
68
|
+
let info;
|
|
69
|
+
try {
|
|
70
|
+
info = await got('https://api.wordpress.org/plugins/info/1.2/', {
|
|
71
|
+
searchParams: { action: 'plugin_information', 'request[slug]': next },
|
|
72
|
+
timeout: { request: 10_000 },
|
|
73
|
+
}).json();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const requires = Array.isArray(info.requires_plugins) ? info.requires_plugins : [];
|
|
79
|
+
for (const dep of requires) {
|
|
80
|
+
if (typeof dep !== 'string' || dep === slug || found.includes(dep)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
found.push(dep);
|
|
84
|
+
queue.push(dep);
|
|
85
|
+
}
|
|
46
86
|
}
|
|
87
|
+
return found;
|
|
47
88
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
2
|
type Advisory = {
|
|
3
|
-
cve?: string;
|
|
4
|
-
fixedIn?: string;
|
|
5
|
-
score?: number;
|
|
6
|
-
severity?: string;
|
|
3
|
+
cve?: string | undefined;
|
|
4
|
+
fixedIn?: string | undefined;
|
|
5
|
+
score?: number | undefined;
|
|
6
|
+
severity?: string | undefined;
|
|
7
7
|
slug: string;
|
|
8
8
|
title: string;
|
|
9
9
|
};
|
|
@@ -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);
|
|
@@ -3,10 +3,11 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { confirmUninstall } from '../../lib/interactive.js';
|
|
6
|
+
import { causeOf, isRecord } from '../../lib/narrow.js';
|
|
6
7
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
-
import { isLocalEnvironment, isNotFoundError } from '../../lib/wp-client.js';
|
|
8
|
+
import { isLocalEnvironment, isNotFoundError, parseBody, responseOf } from '../../lib/wp-client.js';
|
|
8
9
|
import { pinActive, pinVersion, pinVersions, } from '../../types/plugin.js';
|
|
9
|
-
import { requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
10
|
+
import { parseComposerJson, requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
10
11
|
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
11
12
|
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
12
13
|
import { isExactVersion } from '../../utils/version.js';
|
|
@@ -105,10 +106,33 @@ export default class Push extends PushCommand {
|
|
|
105
106
|
status: 'success',
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
// WordPress refuses to activate a plugin whose `Requires Plugins` are inactive, and loopress.json
|
|
110
|
+
// has no dependency order (dokan-lite sorts before woocommerce). A refused plugin is retried
|
|
111
|
+
// after the others, until a round activates nothing.
|
|
108
112
|
async activate(plugins) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
let pending = plugins;
|
|
114
|
+
while (pending.length > 0) {
|
|
115
|
+
const blocked = [];
|
|
116
|
+
let missing = [];
|
|
117
|
+
for (const plugin of pending) {
|
|
118
|
+
this.log(` ⊙ activating ${plugin.slug}`);
|
|
119
|
+
try {
|
|
120
|
+
await this.wp.put(`wp/v2/plugins/${plugin.file}`, { status: 'active' });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const notInstalled = missingDependencies(error);
|
|
124
|
+
if (!notInstalled)
|
|
125
|
+
throw error;
|
|
126
|
+
blocked.push(plugin);
|
|
127
|
+
missing = [...missing, ...notInstalled];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (blocked.length === pending.length) {
|
|
131
|
+
const hint = missing.length > 0 ? ` Not installed: ${[...new Set(missing)].join(', ')}.` : '';
|
|
132
|
+
this.error(`Could not activate ${blocked.map((p) => p.slug).join(', ')}: WordPress reports required plugins missing or inactive.${hint} ` +
|
|
133
|
+
'Add them with `lps plugin add <slug>`, then push again.');
|
|
134
|
+
}
|
|
135
|
+
pending = blocked;
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
async deactivate(plugins) {
|
|
@@ -177,7 +201,7 @@ export default class Push extends PushCommand {
|
|
|
177
201
|
async loadSource(activate) {
|
|
178
202
|
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
179
203
|
if (existsSync(composerJsonPath)) {
|
|
180
|
-
const composerJson =
|
|
204
|
+
const composerJson = parseComposerJson(await readFile(composerJsonPath, 'utf8'));
|
|
181
205
|
const includeDev = isLocalEnvironment(this.siteConfig.name);
|
|
182
206
|
const manifest = wpackagistRequire(composerJson, 'plugin', includeDev);
|
|
183
207
|
if (Object.keys(manifest).length === 0) {
|
|
@@ -232,6 +256,17 @@ export default class Push extends PushCommand {
|
|
|
232
256
|
}
|
|
233
257
|
}
|
|
234
258
|
}
|
|
259
|
+
// The dependency slugs not installed at all when WordPress refused an activation over unmet
|
|
260
|
+
// `Requires Plugins` (empty when they are only inactive), or null for any other error. The REST
|
|
261
|
+
// controller adds `{status: 500}` as the error data, which pushes activate_plugin()'s own data
|
|
262
|
+
// (`{not_installed, inactive}`) into `additional_data`.
|
|
263
|
+
function missingDependencies(error) {
|
|
264
|
+
const body = parseBody(responseOf(causeOf(error)).body);
|
|
265
|
+
if (body.code !== 'plugin_missing_dependencies')
|
|
266
|
+
return null;
|
|
267
|
+
const data = [body.data, ...(Array.isArray(body.additional_data) ? body.additional_data : [])];
|
|
268
|
+
return data.flatMap((d) => (isRecord(d) && isRecord(d.not_installed) ? Object.keys(d.not_installed) : []));
|
|
269
|
+
}
|
|
235
270
|
function isNoop(diff, toPrune) {
|
|
236
271
|
return (diff.toInstall.length === 0 &&
|
|
237
272
|
diff.toPin.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';
|
|
@@ -92,7 +94,6 @@ export default class Config extends Command {
|
|
|
92
94
|
}
|
|
93
95
|
this.log(`✓ "${projectName}/${envName}" configured`);
|
|
94
96
|
await this.maybeBootstrapLoopressFull(env);
|
|
95
|
-
this.log('→ Run `lps project switch` to change the active project or environment');
|
|
96
97
|
}
|
|
97
98
|
async maybeBootstrapLoopressFull(env) {
|
|
98
99
|
const { name, token, url } = env;
|
|
@@ -121,7 +122,7 @@ export default class Config extends Command {
|
|
|
121
122
|
}));
|
|
122
123
|
}
|
|
123
124
|
catch (error) {
|
|
124
|
-
this.warn(error
|
|
125
|
+
this.warn(errorMessage(error));
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
async promptManualCredentials() {
|
|
@@ -157,7 +158,7 @@ export default class Config extends Command {
|
|
|
157
158
|
return { appPassword: password, user: userLogin };
|
|
158
159
|
}
|
|
159
160
|
catch (error) {
|
|
160
|
-
this.warn(`${error
|
|
161
|
+
this.warn(`${errorMessage(error)}\nFalling back to manual credential entry.`);
|
|
161
162
|
return this.promptManualCredentials();
|
|
162
163
|
}
|
|
163
164
|
}
|
|
@@ -171,10 +172,9 @@ export default class Config extends Command {
|
|
|
171
172
|
],
|
|
172
173
|
message: 'Project',
|
|
173
174
|
});
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
const project = projects.find((p) => p.id === choice);
|
|
176
|
+
if (project)
|
|
176
177
|
return { projectId: project.id, projectName: project.name };
|
|
177
|
-
}
|
|
178
178
|
}
|
|
179
179
|
const existingNames = new Set(projects.map((project) => project.name.trim().toLowerCase()));
|
|
180
180
|
const projectName = await input({
|
|
@@ -197,7 +197,7 @@ export default class Config extends Command {
|
|
|
197
197
|
this.log(`✓ Removed ${pluralize(removed.length, 'leftover temporary admin account')}: ${removed.join(', ')}`);
|
|
198
198
|
}
|
|
199
199
|
catch (error) {
|
|
200
|
-
this.warn(error
|
|
200
|
+
this.warn(errorMessage(error));
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
}
|
|
@@ -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
|
}
|
|
@@ -14,6 +14,7 @@ export default class Promote extends Command {
|
|
|
14
14
|
static examples: string[];
|
|
15
15
|
static flags: {
|
|
16
16
|
overwrite: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
'skip-unsupported': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
18
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
19
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
20
|
};
|