@loopress/cli 0.27.0 → 0.29.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 +448 -189
- package/bin/dev.js +0 -0
- package/dist/commands/acf/pull.d.ts +9 -1
- package/dist/commands/acf/pull.js +2 -0
- package/dist/commands/acf/push.d.ts +9 -1
- package/dist/commands/acf/push.js +3 -1
- package/dist/commands/api/publish.js +1 -1
- package/dist/commands/app/pull.js +17 -4
- package/dist/commands/app/push.d.ts +3 -0
- package/dist/commands/app/push.js +30 -6
- package/dist/commands/composer/push.js +3 -21
- package/dist/commands/diff.js +1 -1
- package/dist/commands/form/pull.d.ts +9 -1
- package/dist/commands/form/pull.js +3 -1
- package/dist/commands/form/push.d.ts +7 -1
- package/dist/commands/form/push.js +3 -1
- package/dist/commands/init.js +6 -36
- package/dist/commands/menu/pull.d.ts +7 -1
- package/dist/commands/menu/pull.js +2 -0
- package/dist/commands/menu/push.d.ts +7 -1
- package/dist/commands/menu/push.js +3 -1
- package/dist/commands/option/pull.js +4 -4
- package/dist/commands/option/push.js +2 -0
- package/dist/commands/page/push.js +3 -0
- package/dist/commands/plugin/add.js +6 -3
- package/dist/commands/plugin/audit.js +4 -5
- package/dist/commands/plugin/pull.d.ts +7 -1
- package/dist/commands/plugin/pull.js +68 -14
- package/dist/commands/plugin/push.d.ts +5 -1
- package/dist/commands/plugin/push.js +92 -38
- package/dist/commands/plugin/status.d.ts +1 -0
- package/dist/commands/plugin/status.js +21 -2
- package/dist/commands/project/config.js +12 -8
- package/dist/commands/promote.d.ts +2 -0
- package/dist/commands/promote.js +69 -8
- package/dist/commands/pull.js +2 -2
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +40 -5
- package/dist/commands/seo/pull.d.ts +8 -1
- package/dist/commands/seo/pull.js +2 -0
- package/dist/commands/seo/push.d.ts +7 -1
- package/dist/commands/seo/push.js +3 -1
- package/dist/commands/snippet/publish.js +1 -1
- package/dist/commands/snippet/pull.js +1 -1
- package/dist/commands/snippet/push.js +1 -1
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +4 -4
- package/dist/commands/theme/pull.d.ts +6 -8
- package/dist/commands/theme/pull.js +30 -26
- package/dist/commands/theme/push.d.ts +8 -9
- package/dist/commands/theme/push.js +54 -111
- package/dist/commands/theme/style/diff.d.ts +2 -0
- package/dist/commands/{theme-styles → theme/style}/diff.js +2 -1
- package/dist/commands/{theme-styles → theme/style}/pull.d.ts +8 -2
- package/dist/commands/{theme-styles → theme/style}/pull.js +6 -4
- package/dist/commands/{theme-styles → theme/style}/push.d.ts +9 -2
- package/dist/commands/{theme-styles → theme/style}/push.js +9 -6
- package/dist/commands/theme/style/rollback.d.ts +2 -0
- package/dist/commands/theme/style/rollback.js +6 -0
- package/dist/commands/theme/template/diff.d.ts +10 -0
- package/dist/commands/theme/template/diff.js +13 -0
- package/dist/commands/theme/template/list.d.ts +13 -0
- package/dist/commands/theme/template/list.js +23 -0
- package/dist/commands/theme/template/push.d.ts +20 -0
- package/dist/commands/theme/template/push.js +44 -0
- package/dist/commands/theme/version/pull.d.ts +23 -0
- package/dist/commands/theme/version/pull.js +43 -0
- package/dist/commands/theme/version/push.d.ts +22 -0
- package/dist/commands/theme/version/push.js +123 -0
- package/dist/hooks/finally.d.ts +2 -1
- package/dist/hooks/finally.js +27 -4
- package/dist/lib/api-client.js +3 -1
- package/dist/lib/app-build.d.ts +5 -0
- package/dist/lib/app-build.js +69 -0
- package/dist/lib/app-manifest.d.ts +4 -0
- package/dist/lib/app-manifest.js +10 -2
- package/dist/lib/base.d.ts +1 -0
- package/dist/lib/base.js +14 -2
- package/dist/lib/bootstrap-full-install.js +45 -14
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +3 -1
- package/dist/lib/diff-command.d.ts +1 -0
- package/dist/lib/diff-command.js +6 -4
- package/dist/lib/diff-state.js +1 -0
- package/dist/lib/github-release.js +0 -1
- package/dist/lib/local-callback-server.d.ts +12 -6
- package/dist/lib/local-callback-server.js +21 -22
- package/dist/lib/php-files-command.js +3 -1
- package/dist/lib/push-command.d.ts +5 -0
- package/dist/lib/push-command.js +30 -8
- package/dist/lib/resource-state.js +47 -4
- package/dist/lib/rollback-command.d.ts +1 -0
- package/dist/lib/rollback-command.js +8 -6
- package/dist/lib/rotate-app-password.js +2 -2
- package/dist/lib/snapshot-store.js +2 -2
- package/dist/lib/validate-local.js +6 -0
- package/dist/lib/wp-authorize-flow.d.ts +10 -17
- package/dist/lib/wp-authorize-flow.js +24 -28
- package/dist/lib/wp-client.d.ts +4 -1
- package/dist/lib/wp-client.js +25 -1
- package/dist/lib/wp-site-diagnostic.js +37 -0
- package/dist/types/plugin.d.ts +8 -1
- package/dist/types/plugin.js +4 -1
- package/dist/types/project-config.generated.d.ts +19 -2
- package/dist/utils/composer.d.ts +16 -0
- package/dist/utils/composer.js +144 -0
- package/dist/utils/option-format.d.ts +3 -1
- package/dist/utils/option-format.js +14 -3
- package/dist/utils/page-format.d.ts +12 -0
- package/dist/utils/page-format.js +40 -28
- package/dist/utils/plugin-sync.js +1 -1
- package/dist/utils/plugins.d.ts +9 -4
- package/dist/utils/plugins.js +16 -7
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +6 -2
- package/dist/utils/seo-format.d.ts +2 -2
- package/dist/utils/template-format.d.ts +31 -0
- package/dist/utils/template-format.js +43 -0
- package/oclif.manifest.json +841 -63
- package/package.json +24 -17
- package/schemas/project-config.schema.json +25 -4
- package/dist/commands/theme-styles/diff.d.ts +0 -2
- package/dist/commands/theme-styles/rollback.d.ts +0 -2
- package/dist/commands/theme-styles/rollback.js +0 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { pinVersion } from '../../types/plugin.js';
|
|
3
4
|
import { compareVersions } from '../../utils/version.js';
|
|
4
5
|
const WPVULN_BASE = 'https://www.wpvulnerability.net/plugin';
|
|
5
6
|
const WPORG_INFO = 'https://api.wordpress.org/plugins/info/1.2/';
|
|
@@ -18,7 +19,7 @@ export default class Audit extends LoopressCommand {
|
|
|
18
19
|
const advisories = [];
|
|
19
20
|
const health = [];
|
|
20
21
|
for (const slug of slugs) {
|
|
21
|
-
const found = await this.auditOne(slug, manifest[slug]);
|
|
22
|
+
const found = await this.auditOne(slug, pinVersion(manifest[slug]));
|
|
22
23
|
advisories.push(...found.advisories);
|
|
23
24
|
health.push(...found.health);
|
|
24
25
|
}
|
|
@@ -26,16 +27,14 @@ export default class Audit extends LoopressCommand {
|
|
|
26
27
|
}
|
|
27
28
|
async auditOne(slug, pinned) {
|
|
28
29
|
const [vulns, info] = await Promise.all([this.fetchVulnerabilities(slug), this.fetchHealth(slug)]);
|
|
29
|
-
const advisories = vulns
|
|
30
|
-
.filter((v) => pinned === 'latest' || affects(pinned, v.fixedIn))
|
|
31
|
-
.map((v) => ({ ...v, slug }));
|
|
30
|
+
const advisories = vulns.filter((v) => pinned === 'latest' || affects(pinned, v.fixedIn)).map((v) => ({ ...v, slug }));
|
|
32
31
|
const health = info ? describeHealth(slug, pinned, info) : [];
|
|
33
32
|
return { advisories, health };
|
|
34
33
|
}
|
|
35
34
|
async fetchHealth(slug) {
|
|
36
35
|
try {
|
|
37
36
|
const body = await got(WPORG_INFO, {
|
|
38
|
-
searchParams: {
|
|
37
|
+
searchParams: { action: 'plugin_information', 'request[slug]': slug },
|
|
39
38
|
timeout: { request: 10_000 },
|
|
40
39
|
}).json();
|
|
41
40
|
return typeof body === 'object' && !('error' in body) ? body : null;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type ComposerPullResult } from '../../utils/composer.js';
|
|
2
3
|
import { type MergeResult } from '../../utils/plugins.js';
|
|
3
4
|
type PullResult = MergeResult & {
|
|
4
|
-
|
|
5
|
+
skipped?: ComposerPullResult['skipped'];
|
|
6
|
+
status: 'dry-run' | 'success';
|
|
7
|
+
untracked: string[];
|
|
5
8
|
};
|
|
6
9
|
export default class Pull extends LoopressCommand {
|
|
7
10
|
static description: string;
|
|
8
11
|
static enableJsonFlag: boolean;
|
|
9
12
|
static examples: string[];
|
|
10
13
|
static flags: {
|
|
14
|
+
plugin: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
17
|
};
|
|
13
18
|
run(): Promise<PullResult>;
|
|
19
|
+
private selectPlugins;
|
|
14
20
|
}
|
|
15
21
|
export {};
|
|
@@ -1,36 +1,68 @@
|
|
|
1
|
+
import { checkbox } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
1
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
2
5
|
import { join } from 'node:path';
|
|
3
6
|
import { LoopressCommand } from '../../lib/base.js';
|
|
7
|
+
import { isInteractive } from '../../lib/interactive.js';
|
|
8
|
+
import { getComposerManagedSlugs, NOT_ON_WORDPRESS_ORG, pullIntoComposerJson, } from '../../utils/composer.js';
|
|
4
9
|
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
5
10
|
import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
6
11
|
export default class Pull extends LoopressCommand {
|
|
7
|
-
static description = 'Pull installed plugins from WordPress into loopress.json, pinned to their live versions'
|
|
12
|
+
static description = 'Pull installed plugins from WordPress into loopress.json (or composer.json), pinned to their live versions. ' +
|
|
13
|
+
'Plugins already tracked are refreshed; new ones are offered as a list to tick, so plugins that only belong ' +
|
|
14
|
+
'on this environment (cache, security, backups) stay out of the project.';
|
|
8
15
|
static enableJsonFlag = true;
|
|
9
|
-
static examples = ['$ lps plugin pull', '$ lps plugin pull --
|
|
16
|
+
static examples = ['$ lps plugin pull', '$ lps plugin pull --env production --plugin query-monitor'];
|
|
10
17
|
static flags = {
|
|
11
18
|
...LoopressCommand.dryRunFlag,
|
|
19
|
+
...LoopressCommand.yesFlag,
|
|
20
|
+
plugin: Flags.string({
|
|
21
|
+
description: 'Start tracking this installed plugin too, without asking (repeatable)',
|
|
22
|
+
multiple: true,
|
|
23
|
+
}),
|
|
12
24
|
};
|
|
13
25
|
async run() {
|
|
26
|
+
const { flags } = await this.parse(Pull);
|
|
14
27
|
const { url } = this.siteConfig;
|
|
15
|
-
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
16
|
-
this.warn('This project has a composer.json, which is authoritative for plugins. Run `lps composer pull` instead.');
|
|
17
|
-
return { added: [], merged: this.localConfig.plugins ?? {}, status: 'composer-managed', updated: [] };
|
|
18
|
-
}
|
|
19
28
|
this.log(`Pulling plugins from ${url}`);
|
|
20
29
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
30
|
+
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
31
|
+
const composerJson = existsSync(composerJsonPath)
|
|
32
|
+
? JSON.parse(await readFile(composerJsonPath, 'utf8'))
|
|
33
|
+
: undefined;
|
|
34
|
+
const declared = new Set(composerJson ? getComposerManagedSlugs(composerJson) : Object.keys(this.localConfig.plugins ?? {}));
|
|
35
|
+
const all = parseInstalledPlugins(raw);
|
|
36
|
+
const selected = await this.selectPlugins(all, declared, flags.plugin ?? [], flags.yes);
|
|
37
|
+
const installed = all.filter((p) => selected.has(p.slug));
|
|
38
|
+
const untracked = all.filter((p) => !selected.has(p.slug)).map((p) => p.slug);
|
|
39
|
+
if (untracked.length > 0) {
|
|
40
|
+
this.log(` - Not tracked: ${untracked.join(', ')} (add one with \`lps plugin pull --plugin <slug>\`)`);
|
|
41
|
+
}
|
|
42
|
+
// Pin every plugin to the version actually running on the site, and record the inactive ones
|
|
43
|
+
// as such so a later `plugin push` doesn't switch them on. A later `plugin push` installs
|
|
44
|
+
// exactly this set via Composer + WPackagist; drift only surfaces when the pinned version and
|
|
45
|
+
// the live version disagree.
|
|
46
|
+
const versions = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
|
|
47
|
+
// A composer.json is authoritative over loopress.json: pin the live versions there instead.
|
|
48
|
+
if (composerJson) {
|
|
49
|
+
const result = await pullIntoComposerJson(composerJsonPath, 'plugin', versions, {
|
|
50
|
+
dryRun: this.dryRun,
|
|
51
|
+
log: this.log.bind(this),
|
|
52
|
+
});
|
|
53
|
+
// Ticked, but Composer can't install it (premium or custom): still not tracked by the project.
|
|
54
|
+
const notInstallable = result.skipped.filter((s) => s.reason === NOT_ON_WORDPRESS_ORG).map((s) => s.slug);
|
|
55
|
+
return { ...result, status: this.dryRun ? 'dry-run' : 'success', untracked: [...untracked, ...notInstallable] };
|
|
56
|
+
}
|
|
57
|
+
const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
|
|
58
|
+
const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, pins);
|
|
27
59
|
if (this.dryRun) {
|
|
28
60
|
this.log(`[dry-run] Would write ${Object.keys(merged).length} plugins to loopress.json`);
|
|
29
61
|
if (added.length > 0)
|
|
30
62
|
this.log(` + ${added.join(', ')}`);
|
|
31
63
|
for (const u of updated)
|
|
32
64
|
this.log(` ~ ${u.slug} (${u.from} → ${u.to})`);
|
|
33
|
-
return { added, merged, status: 'dry-run', updated };
|
|
65
|
+
return { added, merged, status: 'dry-run', untracked, updated };
|
|
34
66
|
}
|
|
35
67
|
await writeLocalConfig({ ...this.localConfig, plugins: merged });
|
|
36
68
|
this.log(`Wrote ${Object.keys(merged).length} plugins to loopress.json`);
|
|
@@ -38,6 +70,28 @@ export default class Pull extends LoopressCommand {
|
|
|
38
70
|
this.log(` + Added: ${added.join(', ')}`);
|
|
39
71
|
for (const u of updated)
|
|
40
72
|
this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
41
|
-
return { added, merged, status: 'success', updated };
|
|
73
|
+
return { added, merged, status: 'success', untracked, updated };
|
|
74
|
+
}
|
|
75
|
+
// Plugins already tracked are always refreshed, plus any --plugin. Every other installed plugin
|
|
76
|
+
// is offered unticked, so a second pull from production never brings Wordfence back on its
|
|
77
|
+
// own. With nobody to ask (--yes, --json, CI, the MCP server), only those two sets count:
|
|
78
|
+
// "track everything" would bring the same problem back silently.
|
|
79
|
+
async selectPlugins(installed, declared, requested, yes) {
|
|
80
|
+
const slugs = new Set(installed.map((p) => p.slug));
|
|
81
|
+
for (const slug of requested) {
|
|
82
|
+
if (!slugs.has(slug))
|
|
83
|
+
this.warn(`--plugin ${slug}: not installed on this environment, skipped.`);
|
|
84
|
+
}
|
|
85
|
+
const selected = new Set([...slugs].filter((slug) => declared.has(slug) || requested.includes(slug)));
|
|
86
|
+
const candidates = installed.filter((p) => !selected.has(p.slug));
|
|
87
|
+
if (yes || candidates.length === 0 || this.jsonEnabled() || !isInteractive())
|
|
88
|
+
return selected;
|
|
89
|
+
const picked = await checkbox({
|
|
90
|
+
choices: candidates.map((p) => ({ name: `${p.slug} ${p.version}${p.active ? '' : ' (inactive)'}`, value: p.slug })),
|
|
91
|
+
message: 'Which of these plugins should the project track? Leave the ones that only belong on this environment unticked.',
|
|
92
|
+
});
|
|
93
|
+
for (const slug of picked)
|
|
94
|
+
selected.add(slug);
|
|
95
|
+
return selected;
|
|
42
96
|
}
|
|
43
97
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
2
|
type PushResult = {
|
|
3
3
|
activated: string[];
|
|
4
|
+
deactivated: string[];
|
|
4
5
|
installed: string[];
|
|
5
6
|
pinned: string[];
|
|
6
7
|
pruned: string[];
|
|
7
8
|
removed: string[];
|
|
8
|
-
status: '
|
|
9
|
+
status: 'dry-run' | 'in-sync' | 'success';
|
|
9
10
|
};
|
|
10
11
|
export default class Push extends PushCommand {
|
|
11
12
|
static description: string;
|
|
12
13
|
static enableJsonFlag: boolean;
|
|
13
14
|
static examples: string[];
|
|
14
15
|
static flags: {
|
|
16
|
+
activate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
prune: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
19
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -24,6 +26,8 @@ export default class Push extends PushCommand {
|
|
|
24
26
|
private fetchInstanceLock;
|
|
25
27
|
private filesForNewlyInstalled;
|
|
26
28
|
private guardForce;
|
|
29
|
+
private loadSource;
|
|
30
|
+
private logStillInactive;
|
|
27
31
|
private result;
|
|
28
32
|
private sync;
|
|
29
33
|
}
|
|
@@ -1,86 +1,104 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { confirmUninstall } from '../../lib/interactive.js';
|
|
5
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
|
-
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
-
import {
|
|
7
|
+
import { isLocalEnvironment, isNotFoundError } from '../../lib/wp-client.js';
|
|
8
|
+
import { pinActive, pinVersion, pinVersions, } from '../../types/plugin.js';
|
|
9
|
+
import { requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
10
|
+
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
8
11
|
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
9
|
-
|
|
12
|
+
import { isExactVersion } from '../../utils/version.js';
|
|
13
|
+
const IN_SYNC = {
|
|
14
|
+
activated: [],
|
|
15
|
+
deactivated: [],
|
|
16
|
+
installed: [],
|
|
17
|
+
pinned: [],
|
|
18
|
+
pruned: [],
|
|
19
|
+
removed: [],
|
|
20
|
+
status: 'in-sync',
|
|
21
|
+
};
|
|
10
22
|
export default class Push extends PushCommand {
|
|
11
|
-
static description = 'Install plugins on WordPress to match loopress.json,
|
|
23
|
+
static description = 'Install plugins on WordPress to match loopress.json (or composer.json), via Composer + WPackagist';
|
|
12
24
|
static enableJsonFlag = true;
|
|
13
25
|
static examples = ['$ lps plugin push', '$ lps plugin push --dry-run', '$ lps plugin push --force --prune'];
|
|
14
26
|
static flags = {
|
|
15
27
|
...PushCommand.dryRunFlag,
|
|
16
28
|
...PushCommand.yesFlag,
|
|
29
|
+
activate: Flags.boolean({
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'composer.json projects only: activate every plugin it declares. Without it, plugins keep their current state and new ones stay inactive',
|
|
32
|
+
}),
|
|
17
33
|
force: Flags.boolean({
|
|
18
34
|
default: false,
|
|
19
35
|
description: 'Allow downgrades and let Loopress take over plugins installed outside it (replaces their files)',
|
|
20
36
|
}),
|
|
21
37
|
prune: Flags.boolean({
|
|
22
38
|
default: false,
|
|
23
|
-
description: 'Deactivate plugins that are active on the site but absent from loopress.json',
|
|
39
|
+
description: 'Deactivate plugins that are active on the site but absent from loopress.json (or composer.json)',
|
|
24
40
|
}),
|
|
25
41
|
};
|
|
26
42
|
async run() {
|
|
27
43
|
const { flags } = await this.parse(Push);
|
|
28
|
-
const { force, prune } = flags;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return { ...IN_SYNC, status: 'composer-managed' };
|
|
32
|
-
}
|
|
33
|
-
const manifest = this.localConfig.plugins ?? {};
|
|
34
|
-
if (Object.keys(manifest).length === 0) {
|
|
35
|
-
this.error('No plugins found in loopress.json. Run `lps plugin pull` or `lps plugin add <slug>` first.');
|
|
36
|
-
}
|
|
44
|
+
const { activate, force, prune } = flags;
|
|
45
|
+
const source = await this.loadSource(activate);
|
|
46
|
+
const fromComposer = source.file === 'composer.json';
|
|
37
47
|
this.log(`Pushing plugins to ${this.siteConfig.url}`);
|
|
48
|
+
if (fromComposer) {
|
|
49
|
+
this.log(`From composer.json, pushed whole (${source.packageCount} packages): themes and libraries it declares are synced too.`);
|
|
50
|
+
}
|
|
38
51
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
39
52
|
const installed = parseInstalledPlugins(raw);
|
|
40
53
|
const managed = lockedWpackagistSlugs(await this.fetchInstanceLock(), 'plugin');
|
|
41
|
-
const diff = diffPlugins(manifest, installed, managed);
|
|
42
|
-
|
|
54
|
+
const diff = diffPlugins(source.manifest, installed, managed);
|
|
55
|
+
// composer.json records no active state: leave inactive plugins alone unless --activate.
|
|
56
|
+
const skippedActivations = fromComposer && !activate ? diff.toActivate : [];
|
|
57
|
+
if (fromComposer && !activate)
|
|
58
|
+
diff.toActivate = [];
|
|
59
|
+
this.guardForce(diff, force, source.file);
|
|
43
60
|
const toPrune = prune ? diff.untrackedActive : [];
|
|
44
|
-
// A "latest" pin never shows as drift, but its newest upstream
|
|
45
|
-
// since the last push, so a push must still run `composer update
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
61
|
+
// A "latest" pin (or a Composer constraint) never shows as drift, but its newest upstream
|
|
62
|
+
// release may have moved since the last push, so a push must still run `composer update`.
|
|
63
|
+
const hasFloatingPin = Object.values(source.manifest).some((pin) => !isExactVersion(pinVersion(pin)));
|
|
64
|
+
if (!hasFloatingPin && isNoop(diff, toPrune)) {
|
|
48
65
|
this.log('Everything is already in sync.');
|
|
66
|
+
this.logStillInactive(skippedActivations);
|
|
49
67
|
return { ...IN_SYNC };
|
|
50
68
|
}
|
|
51
69
|
if (isNoop(diff, toPrune))
|
|
52
|
-
this.log('Refreshing plugins pinned to
|
|
70
|
+
this.log('Refreshing plugins not pinned to an exact version to their newest releases.');
|
|
53
71
|
logPlan(this, diff, toPrune, force);
|
|
54
72
|
if (this.dryRun) {
|
|
55
73
|
return this.result(diff, { includeCollisions: true, pruned: toPrune, removed: diff.toRemove, status: 'dry-run' });
|
|
56
74
|
}
|
|
57
75
|
if (!(await confirmUninstall(diff.toRemove, this.yes)))
|
|
58
76
|
this.error('Aborted.');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// being uninstalled) plus the ones the manifest wants active but that are inactive today.
|
|
62
|
-
const toReactivate = [...deactivated.filter((p) => !diff.toRemove.includes(p.slug)), ...diff.toActivate];
|
|
77
|
+
// Plugins switched off for the file swap come back on, unless loopress.json wants them inactive.
|
|
78
|
+
const deactivated = (await this.deactivateEndangered(installed, diff, force)).filter((p) => !diff.toRemove.includes(p.slug) && (fromComposer || pinActive(source.manifest[p.slug])));
|
|
63
79
|
let response;
|
|
64
80
|
try {
|
|
65
|
-
response = await this.sync(
|
|
81
|
+
response = await this.sync(source.intent, force);
|
|
66
82
|
}
|
|
67
83
|
catch (error) {
|
|
68
84
|
// The file swap never happened, so restore the plugins we defensively deactivated
|
|
69
85
|
// instead of leaving the site with them switched off.
|
|
70
|
-
await this.activate(deactivated
|
|
86
|
+
await this.activate(deactivated);
|
|
71
87
|
throw error;
|
|
72
88
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await this.
|
|
89
|
+
const activateNew = (slug) => (fromComposer ? activate : pinActive(source.manifest[slug]));
|
|
90
|
+
const newlyInstalled = await this.filesForNewlyInstalled(diff.toInstall);
|
|
91
|
+
const toActivate = [...deactivated, ...diff.toActivate, ...newlyInstalled.filter((p) => activateNew(p.slug))];
|
|
92
|
+
const toDeactivate = [...diff.toDeactivate, ...installed.filter((p) => toPrune.includes(p.slug))];
|
|
93
|
+
await this.deactivate(toDeactivate);
|
|
94
|
+
await this.activate(toActivate);
|
|
78
95
|
if (response.output.trim())
|
|
79
96
|
this.log(response.output.trim());
|
|
80
97
|
this.log('Plugins synced.');
|
|
98
|
+
this.logStillInactive([...skippedActivations, ...newlyInstalled.filter((p) => !activateNew(p.slug))], fromComposer);
|
|
81
99
|
await this.recordSuccess();
|
|
82
100
|
return this.result(diff, {
|
|
83
|
-
activated:
|
|
101
|
+
activated: toActivate.map((p) => p.slug),
|
|
84
102
|
includeCollisions: force,
|
|
85
103
|
pruned: toPrune,
|
|
86
104
|
removed: response.removed ?? diff.toRemove,
|
|
@@ -140,26 +158,60 @@ export default class Push extends PushCommand {
|
|
|
140
158
|
return file ? [{ file, slug }] : [];
|
|
141
159
|
});
|
|
142
160
|
}
|
|
143
|
-
guardForce(diff, force) {
|
|
161
|
+
guardForce(diff, force, file) {
|
|
144
162
|
if (force)
|
|
145
163
|
return;
|
|
146
164
|
if (diff.collisions.length > 0) {
|
|
147
165
|
const list = diff.collisions.map((c) => `${c.slug} (${c.installedVersion})`).join(', ');
|
|
148
166
|
this.error(`${diff.collisions.length} plugin(s) are already installed outside Loopress: ${list}. ` +
|
|
149
167
|
'Re-run with --force to let Loopress manage them (this replaces their files with the WPackagist build, ' +
|
|
150
|
-
|
|
168
|
+
`local modifications are lost), or remove them from ${file}.`);
|
|
151
169
|
}
|
|
152
170
|
const downgrades = diff.toPin.filter((p) => isDowngrade(p.from, p.to));
|
|
153
171
|
if (downgrades.length > 0) {
|
|
154
172
|
const list = downgrades.map((p) => `${p.slug} ${p.from} to ${p.to}`).join(', ');
|
|
155
173
|
this.error(`Refusing to downgrade: ${list}. A downgrade only replaces plugin files, it does not undo database ` +
|
|
156
|
-
|
|
174
|
+
`migrations the newer version ran, which can break the site. Re-run with --force, or bump the version in ${file}.`);
|
|
157
175
|
}
|
|
158
176
|
}
|
|
177
|
+
async loadSource(activate) {
|
|
178
|
+
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
179
|
+
if (existsSync(composerJsonPath)) {
|
|
180
|
+
const composerJson = JSON.parse(await readFile(composerJsonPath, 'utf8'));
|
|
181
|
+
const includeDev = isLocalEnvironment(this.siteConfig.name);
|
|
182
|
+
const manifest = wpackagistRequire(composerJson, 'plugin', includeDev);
|
|
183
|
+
if (Object.keys(manifest).length === 0) {
|
|
184
|
+
this.error('No wpackagist-plugin/* package in composer.json. Run `lps plugin pull` first, or `lps composer push` to push it as is.');
|
|
185
|
+
}
|
|
186
|
+
const require = requireToInstall(composerJson, includeDev);
|
|
187
|
+
return { file: 'composer.json', intent: toIntent(require), manifest, packageCount: Object.keys(require).length };
|
|
188
|
+
}
|
|
189
|
+
if (activate) {
|
|
190
|
+
this.error('--activate only applies to projects with a composer.json: loopress.json already records whether each plugin is active.');
|
|
191
|
+
}
|
|
192
|
+
const manifest = this.localConfig.plugins ?? {};
|
|
193
|
+
if (Object.keys(manifest).length === 0) {
|
|
194
|
+
this.error('No plugins found in loopress.json. Run `lps plugin pull` or `lps plugin add <slug>` first.');
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
file: 'loopress.json',
|
|
198
|
+
intent: { plugins: pinVersions(manifest) },
|
|
199
|
+
manifest,
|
|
200
|
+
packageCount: Object.keys(manifest).length,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// composer.json projects: plugins this push left inactive, and how to change that.
|
|
204
|
+
logStillInactive(plugins, fromComposer = true) {
|
|
205
|
+
if (!fromComposer || plugins.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
this.log(`${plugins.length} plugin(s) from composer.json are inactive: ${plugins.map((p) => p.slug).join(', ')}.`);
|
|
208
|
+
this.log('Re-run with --activate to activate them, or activate them in wp-admin.');
|
|
209
|
+
}
|
|
159
210
|
result(diff, opts) {
|
|
160
211
|
const collisionSlugs = opts.includeCollisions ? diff.collisions.map((c) => c.slug) : [];
|
|
161
212
|
return {
|
|
162
213
|
activated: opts.activated ?? diff.toActivate.map((a) => a.slug),
|
|
214
|
+
deactivated: diff.toDeactivate.map((a) => a.slug),
|
|
163
215
|
installed: [...diff.toInstall.map((a) => a.slug), ...collisionSlugs],
|
|
164
216
|
pinned: diff.toPin.map((p) => p.slug),
|
|
165
217
|
pruned: opts.pruned,
|
|
@@ -167,9 +219,9 @@ export default class Push extends PushCommand {
|
|
|
167
219
|
status: opts.status,
|
|
168
220
|
};
|
|
169
221
|
}
|
|
170
|
-
async sync(
|
|
222
|
+
async sync(intent, force) {
|
|
171
223
|
try {
|
|
172
|
-
return await this.wp.post('loopress/v1/composer/sync', { force, intent
|
|
224
|
+
return await this.wp.post('loopress/v1/composer/sync', { force, intent, lock: null }, { timeoutMs: SYNC_TIMEOUT_MS });
|
|
173
225
|
}
|
|
174
226
|
catch (error) {
|
|
175
227
|
const collisions = parseCollisions(error);
|
|
@@ -184,6 +236,7 @@ function isNoop(diff, toPrune) {
|
|
|
184
236
|
return (diff.toInstall.length === 0 &&
|
|
185
237
|
diff.toPin.length === 0 &&
|
|
186
238
|
diff.toActivate.length === 0 &&
|
|
239
|
+
diff.toDeactivate.length === 0 &&
|
|
187
240
|
diff.toRemove.length === 0 &&
|
|
188
241
|
diff.collisions.length === 0 &&
|
|
189
242
|
toPrune.length === 0);
|
|
@@ -201,6 +254,7 @@ function logPlan(cmd, diff, prune, force) {
|
|
|
201
254
|
section('To take over', diff.collisions.map((c) => `! ${c.slug} (installed outside Loopress)`));
|
|
202
255
|
section('To re-pin', diff.toPin.map((p) => `~ ${p.slug} ${p.from} to ${p.to}`));
|
|
203
256
|
section('To activate', diff.toActivate.map((a) => `↑ ${a.slug}`));
|
|
257
|
+
section('To deactivate', diff.toDeactivate.map((a) => `↓ ${a.slug}`));
|
|
204
258
|
section('To uninstall', diff.toRemove.map((s) => `- ${s}`));
|
|
205
259
|
section('To deactivate (--prune)', prune.map((s) => `⊘ ${s}`));
|
|
206
260
|
}
|
|
@@ -12,7 +12,14 @@ export default class Status extends LoopressCommand {
|
|
|
12
12
|
const { url } = this.siteConfig;
|
|
13
13
|
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
14
14
|
this.warn('This project has a composer.json, which is authoritative for plugins. Run `lps composer` commands instead.');
|
|
15
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
drift: false,
|
|
17
|
+
inactive: [],
|
|
18
|
+
missing: [],
|
|
19
|
+
status: 'composer-managed',
|
|
20
|
+
untrackedActive: [],
|
|
21
|
+
wrongVersion: [],
|
|
22
|
+
};
|
|
16
23
|
}
|
|
17
24
|
const manifest = this.localConfig.plugins ?? {};
|
|
18
25
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
@@ -22,6 +29,7 @@ export default class Status extends LoopressCommand {
|
|
|
22
29
|
const missing = diff.toInstall.map((a) => a.slug);
|
|
23
30
|
const wrongVersion = diff.toPin.map((p) => ({ live: p.from, pinned: p.to, slug: p.slug }));
|
|
24
31
|
const inactive = diff.toActivate.map((a) => a.slug);
|
|
32
|
+
const unexpectedlyActive = diff.toDeactivate.map((a) => a.slug);
|
|
25
33
|
const { untrackedActive } = diff;
|
|
26
34
|
// `collisions` are also drift (Loopress doesn't own them yet); surface them under missing's
|
|
27
35
|
// banner so `plugin push --force` is the obvious next step.
|
|
@@ -29,6 +37,7 @@ export default class Status extends LoopressCommand {
|
|
|
29
37
|
const drift = missing.length > 0 ||
|
|
30
38
|
wrongVersion.length > 0 ||
|
|
31
39
|
inactive.length > 0 ||
|
|
40
|
+
unexpectedlyActive.length > 0 ||
|
|
32
41
|
diff.toRemove.length > 0 ||
|
|
33
42
|
collisions.length > 0;
|
|
34
43
|
if (!drift && untrackedActive.length === 0) {
|
|
@@ -43,13 +52,23 @@ export default class Status extends LoopressCommand {
|
|
|
43
52
|
this.log(`Version drift: ${w.slug} is ${w.live}, loopress.json pins ${w.pinned}`);
|
|
44
53
|
if (inactive.length > 0)
|
|
45
54
|
this.log(`Pinned but inactive: ${inactive.join(', ')}`);
|
|
55
|
+
if (unexpectedlyActive.length > 0)
|
|
56
|
+
this.log(`Pinned inactive but active: ${unexpectedlyActive.join(', ')}`);
|
|
46
57
|
if (diff.toRemove.length > 0)
|
|
47
58
|
this.log(`Managed by Loopress but dropped from loopress.json: ${diff.toRemove.join(', ')}`);
|
|
48
59
|
if (untrackedActive.length > 0)
|
|
49
60
|
this.log(`Active but untracked: ${untrackedActive.join(', ')}`);
|
|
50
61
|
if (drift)
|
|
51
62
|
this.exit(1);
|
|
52
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
drift,
|
|
65
|
+
inactive,
|
|
66
|
+
missing,
|
|
67
|
+
status: drift ? 'drift' : 'in-sync',
|
|
68
|
+
unexpectedlyActive,
|
|
69
|
+
untrackedActive,
|
|
70
|
+
wrongVersion,
|
|
71
|
+
};
|
|
53
72
|
}
|
|
54
73
|
async fetchInstanceLock() {
|
|
55
74
|
try {
|
|
@@ -5,7 +5,7 @@ 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
7
|
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
8
|
-
import { WpClient } from '../../lib/wp-client.js';
|
|
8
|
+
import { isLocalEnvironment, WpClient } from '../../lib/wp-client.js';
|
|
9
9
|
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
10
10
|
const NEW_PROJECT = '__new__';
|
|
11
11
|
const AUTH_BROWSER = 'browser';
|
|
@@ -48,16 +48,20 @@ export default class Config extends Command {
|
|
|
48
48
|
const rawUrl = await input({
|
|
49
49
|
message: 'WordPress URL',
|
|
50
50
|
validate(value) {
|
|
51
|
+
let parsed;
|
|
51
52
|
try {
|
|
52
|
-
|
|
53
|
-
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
54
|
-
return 'URL must start with http:// or https://';
|
|
55
|
-
}
|
|
56
|
-
return true;
|
|
53
|
+
parsed = new URL(value);
|
|
57
54
|
}
|
|
58
55
|
catch {
|
|
59
56
|
return 'Invalid URL';
|
|
60
57
|
}
|
|
58
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
59
|
+
return 'URL must start with http:// or https://';
|
|
60
|
+
}
|
|
61
|
+
if (parsed.protocol === 'http:' && !isLocalEnvironment(envName)) {
|
|
62
|
+
return `Plain HTTP would send the Application Password in cleartext. Use HTTPS. Plain HTTP is allowed only for the "local" environment (this one is "${envName}").`;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
61
65
|
},
|
|
62
66
|
});
|
|
63
67
|
let url = rawUrl;
|
|
@@ -88,10 +92,10 @@ export default class Config extends Command {
|
|
|
88
92
|
this.log('→ Run `lps project switch` to change the active project or environment');
|
|
89
93
|
}
|
|
90
94
|
async maybeBootstrapLoopressFull(env) {
|
|
91
|
-
const { token, url } = env;
|
|
95
|
+
const { name, token, url } = env;
|
|
92
96
|
if (!token)
|
|
93
97
|
return; // env was just built with one a few lines up; guard only narrows the type
|
|
94
|
-
const wp = new WpClient(url, token);
|
|
98
|
+
const wp = new WpClient(url, token, name);
|
|
95
99
|
// A detection failure (network hiccup, plugins REST disabled) is treated as "assume it's
|
|
96
100
|
// there" rather than surprising the user with an account-creating prompt they didn't ask
|
|
97
101
|
// for; `lps project config` can simply be re-run if it genuinely isn't installed.
|
|
@@ -17,8 +17,10 @@ export default class Promote extends Command {
|
|
|
17
17
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
18
|
};
|
|
19
19
|
run(): Promise<PromoteResult>;
|
|
20
|
+
private assertDirsInsideProject;
|
|
20
21
|
private confirmPromotion;
|
|
21
22
|
private delegateArgv;
|
|
23
|
+
private enterScratchCopy;
|
|
22
24
|
private requireEnvironment;
|
|
23
25
|
private resolveProject;
|
|
24
26
|
}
|