@loopress/cli 0.28.0 → 0.30.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 +1460 -337
- 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/push.js +1 -1
- package/dist/commands/composer/push.js +3 -3
- package/dist/commands/cpt/diff.js +5 -0
- package/dist/commands/cpt/list.d.ts +2 -0
- package/dist/commands/cpt/list.js +4 -0
- package/dist/commands/cpt/pull.d.ts +2 -0
- package/dist/commands/cpt/pull.js +4 -0
- package/dist/commands/cpt/push.d.ts +2 -0
- package/dist/commands/cpt/push.js +4 -0
- package/dist/commands/cpt/rm.d.ts +2 -0
- package/dist/commands/cpt/rm.js +3 -0
- package/dist/commands/cpt/rollback.js +5 -0
- package/dist/commands/dev.js +14 -4
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +30 -2
- 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 -38
- 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/meta/diff.d.ts +2 -0
- package/dist/commands/meta/diff.js +5 -0
- package/dist/commands/meta/list.d.ts +19 -0
- package/dist/commands/meta/list.js +21 -0
- package/dist/commands/meta/pull.d.ts +2 -0
- package/dist/commands/meta/pull.js +4 -0
- package/dist/commands/meta/push.d.ts +2 -0
- package/dist/commands/meta/push.js +4 -0
- package/dist/commands/meta/rm.d.ts +2 -0
- package/dist/commands/meta/rm.js +3 -0
- package/dist/commands/meta/rollback.d.ts +2 -0
- package/dist/commands/meta/rollback.js +5 -0
- package/dist/commands/option/list.js +2 -8
- package/dist/commands/option/pull.js +4 -4
- package/dist/commands/option/push.js +2 -0
- package/dist/commands/page/push.js +3 -0
- package/dist/commands/pattern/diff.d.ts +2 -0
- package/dist/commands/pattern/diff.js +5 -0
- package/dist/commands/pattern/list.d.ts +2 -0
- package/dist/commands/pattern/list.js +3 -0
- package/dist/commands/pattern/pull.d.ts +2 -0
- package/dist/commands/pattern/pull.js +4 -0
- package/dist/commands/pattern/push.d.ts +2 -0
- package/dist/commands/pattern/push.js +4 -0
- package/dist/commands/pattern/rm.d.ts +2 -0
- package/dist/commands/pattern/rm.js +3 -0
- package/dist/commands/pattern/rollback.d.ts +2 -0
- package/dist/commands/pattern/rollback.js +5 -0
- package/dist/commands/plugin/audit.js +2 -1
- package/dist/commands/plugin/pull.d.ts +4 -0
- package/dist/commands/plugin/pull.js +56 -10
- package/dist/commands/plugin/push.js +5 -4
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +20 -1
- package/dist/commands/project/list.d.ts +1 -1
- package/dist/commands/project/list.js +6 -6
- package/dist/commands/project/pull.js +11 -8
- package/dist/commands/project/push.js +23 -18
- package/dist/commands/project/remove.js +1 -1
- package/dist/commands/project/switch.d.ts +0 -1
- package/dist/commands/project/switch.js +16 -36
- package/dist/commands/pull.js +7 -2
- package/dist/commands/push.js +14 -6
- package/dist/commands/role/diff.d.ts +2 -0
- package/dist/commands/role/diff.js +5 -0
- package/dist/commands/role/list.d.ts +2 -0
- package/dist/commands/role/list.js +4 -0
- package/dist/commands/role/pull.d.ts +2 -0
- package/dist/commands/role/pull.js +4 -0
- package/dist/commands/role/push.d.ts +2 -0
- package/dist/commands/role/push.js +4 -0
- package/dist/commands/role/rm.d.ts +2 -0
- package/dist/commands/role/rm.js +3 -0
- package/dist/commands/role/rollback.d.ts +2 -0
- package/dist/commands/role/rollback.js +5 -0
- package/dist/commands/snippet/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 +3 -3
- package/dist/commands/status.js +21 -53
- package/dist/commands/taxonomy/diff.d.ts +2 -0
- package/dist/commands/taxonomy/diff.js +5 -0
- package/dist/commands/taxonomy/list.d.ts +2 -0
- package/dist/commands/taxonomy/list.js +5 -0
- package/dist/commands/taxonomy/pull.d.ts +2 -0
- package/dist/commands/taxonomy/pull.js +4 -0
- package/dist/commands/taxonomy/push.d.ts +2 -0
- package/dist/commands/taxonomy/push.js +4 -0
- package/dist/commands/taxonomy/rm.d.ts +2 -0
- package/dist/commands/taxonomy/rm.js +3 -0
- package/dist/commands/taxonomy/rollback.d.ts +2 -0
- package/dist/commands/taxonomy/rollback.js +5 -0
- package/dist/commands/theme/pull.d.ts +6 -10
- package/dist/commands/theme/pull.js +30 -32
- package/dist/commands/theme/push.d.ts +8 -9
- package/dist/commands/theme/push.js +54 -111
- package/dist/commands/theme/style/pull.d.ts +7 -1
- package/dist/commands/theme/style/pull.js +3 -1
- package/dist/commands/theme/style/push.d.ts +8 -1
- package/dist/commands/theme/style/push.js +4 -1
- package/dist/commands/{template → theme/template}/diff.d.ts +1 -1
- package/dist/commands/{template → theme/template}/diff.js +3 -3
- package/dist/commands/{template → theme/template}/list.d.ts +2 -2
- package/dist/commands/{template → theme/template}/list.js +5 -5
- package/dist/commands/{template → theme/template}/push.d.ts +3 -1
- package/dist/commands/theme/template/push.js +65 -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/config/project-config.manager.d.ts +3 -3
- package/dist/config/project-config.manager.js +22 -31
- package/dist/hooks/finally.d.ts +2 -1
- package/dist/hooks/finally.js +38 -7
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/app-build.js +3 -1
- package/dist/lib/app-manifest.js +2 -2
- package/dist/lib/base.d.ts +2 -2
- package/dist/lib/base.js +33 -38
- package/dist/lib/declared-type-command.d.ts +35 -0
- package/dist/lib/declared-type-command.js +192 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +7 -3
- package/dist/lib/diff-state.js +1 -0
- package/dist/lib/github-release.js +4 -1
- package/dist/lib/local-callback-server.d.ts +12 -6
- package/dist/lib/local-callback-server.js +24 -23
- package/dist/lib/php-files-command.js +3 -1
- package/dist/lib/push-command.d.ts +6 -0
- package/dist/lib/push-command.js +61 -18
- package/dist/lib/put-or-create.js +3 -4
- package/dist/lib/resource-state.js +40 -86
- package/dist/lib/rollback-materialize.js +13 -24
- package/dist/lib/snapshot-store.js +2 -2
- package/dist/lib/spinner.d.ts +2 -0
- package/dist/lib/spinner.js +72 -0
- package/dist/lib/temp-admin.d.ts +10 -1
- package/dist/lib/temp-admin.js +27 -2
- package/dist/lib/validate-local.js +31 -9
- 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 +1 -0
- package/dist/lib/wp-client.js +7 -1
- package/dist/lib/wp-site-diagnostic.js +39 -1
- package/dist/types/global-config.generated.d.ts +1 -5
- package/dist/types/project-config.generated.d.ts +28 -4
- package/dist/utils/composer.d.ts +3 -1
- package/dist/utils/composer.js +16 -7
- package/dist/utils/declared-type-format.d.ts +52 -0
- package/dist/utils/declared-type-format.js +233 -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 +5 -1
- package/dist/utils/page-format.js +7 -3
- package/dist/utils/plugin-sync.js +1 -1
- package/dist/utils/resource-dirs.d.ts +9 -3
- package/dist/utils/resource-dirs.js +18 -6
- package/dist/utils/template-format.d.ts +3 -1
- package/dist/utils/template-format.js +10 -3
- package/dist/utils/theme-css.d.ts +19 -0
- package/dist/utils/theme-css.js +100 -0
- package/oclif.manifest.json +3542 -1087
- package/package.json +40 -21
- package/schemas/global-config.schema.json +2 -6
- package/schemas/project-config.schema.json +37 -7
- package/dist/commands/seo/diff.js +0 -5
- package/dist/commands/seo/list.d.ts +0 -18
- package/dist/commands/seo/list.js +0 -58
- package/dist/commands/seo/pull.d.ts +0 -17
- package/dist/commands/seo/pull.js +0 -87
- package/dist/commands/seo/push.d.ts +0 -23
- package/dist/commands/seo/push.js +0 -176
- package/dist/commands/seo/rollback.js +0 -5
- package/dist/commands/template/push.js +0 -42
- package/dist/utils/seo-format.d.ts +0 -29
- package/dist/utils/seo-format.js +0 -18
- /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
- /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredRmCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { META_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredRmCommand(META_SPEC, 'Stop registering the Loopress meta keys of one post type (its meta/<post-type>.json file) on WordPress. Their values are kept in the database.', "Their values are kept in the database, hidden until it's pushed again.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags, ux } from '@oclif/core';
|
|
2
2
|
import ttyTable from 'tty-table';
|
|
3
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
4
5
|
import { OPTIONS_ENDPOINT } from '../../utils/option-format.js';
|
|
5
6
|
import { pluralize } from '../../utils/pluralize.js';
|
|
6
7
|
// Same green/yellow pairing drives both SOURCE? and PLUGIN, so a row's whole "where did this come
|
|
@@ -127,14 +128,7 @@ export default class List extends LoopressCommand {
|
|
|
127
128
|
// Every call scans active plugins' own PHP source for names the naming guess missed (not
|
|
128
129
|
// opt-in, roughly 1-3s on a real site once vendor/tests/languages are pruned from the scan;
|
|
129
130
|
// see OptionsService::listOptionNames() server-side), hence the spinner unconditionally.
|
|
130
|
-
|
|
131
|
-
let options;
|
|
132
|
-
try {
|
|
133
|
-
options = await this.wp.get(OPTIONS_ENDPOINT);
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
ux.action.stop();
|
|
137
|
-
}
|
|
131
|
+
let options = await withSpinner('Fetching and verifying options', async () => this.wp.get(OPTIONS_ENDPOINT));
|
|
138
132
|
if (flags['no-core'])
|
|
139
133
|
options = options.filter((option) => !option.core);
|
|
140
134
|
if (options.length === 0) {
|
|
@@ -56,19 +56,19 @@ export default class Pull extends LoopressCommand {
|
|
|
56
56
|
}
|
|
57
57
|
// Returns the refreshed option (and writes it to disk), or null when the tracked name is a
|
|
58
58
|
// 404 on this environment: absent, not a failure (see the orphan reconciliation in run()).
|
|
59
|
-
// readonly
|
|
60
|
-
// existing file rather than overwritten by the pulled value.
|
|
59
|
+
// readonly and refs are local declarations with no WordPress counterpart, always preserved
|
|
60
|
+
// from the existing file rather than overwritten by the pulled value.
|
|
61
61
|
async pullOption(dir, option) {
|
|
62
62
|
let remote;
|
|
63
63
|
try {
|
|
64
|
-
remote = await this.wp.get(optionEndpoint(option.name));
|
|
64
|
+
remote = await this.wp.get(optionEndpoint(option.name, option.refs));
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
if (isNotFoundError(error))
|
|
68
68
|
return null;
|
|
69
69
|
throw error;
|
|
70
70
|
}
|
|
71
|
-
const local = { ...remote, readonly: option.readonly };
|
|
71
|
+
const local = { ...remote, readonly: option.readonly, refs: option.refs };
|
|
72
72
|
await writeFile(join(dir, optionFileName(option.name)), JSON.stringify(local, null, 2) + '\n');
|
|
73
73
|
return local;
|
|
74
74
|
}
|
|
@@ -78,6 +78,8 @@ export default class Push extends PushCommand {
|
|
|
78
78
|
// create): falls back to today's unconditional write, same as before this existed.
|
|
79
79
|
const expectedRevision = await this.currentRevision(option.name);
|
|
80
80
|
const body = { autoload: option.autoload, value: option.value };
|
|
81
|
+
if (option.refs)
|
|
82
|
+
body.refs = option.refs;
|
|
81
83
|
if (expectedRevision !== undefined)
|
|
82
84
|
body.expectedRevision = expectedRevision;
|
|
83
85
|
await this.wp.put(optionEndpoint(option.name), body);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { PushCommand } from '../../lib/push-command.js';
|
|
3
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
3
4
|
import { formatPageProblems, frontPageNote, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
|
|
4
5
|
import { pluralize } from '../../utils/pluralize.js';
|
|
5
6
|
import { resolveResourceDir } from '../../utils/resource-dirs.js';
|
|
@@ -28,6 +29,8 @@ export default class Push extends PushCommand {
|
|
|
28
29
|
this.error(`No page "${args.slug}" in ${path} (expected ${args.slug}.html).`);
|
|
29
30
|
}
|
|
30
31
|
this.log(`Found ${pluralize(selected.length, 'page')} to push`);
|
|
32
|
+
if (this.dryRun)
|
|
33
|
+
await this.previewRemote(getResourceStateProvider('page'), path);
|
|
31
34
|
const pushed = [];
|
|
32
35
|
await this.runPushTasks(selected, (page) => page.slug, async (page, task) => {
|
|
33
36
|
await this.pushPage(page, task);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('pattern', {
|
|
3
|
+
description: 'Show what differs in block patterns between your local patterns/ files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'patterns directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredListCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredListCommand(PATTERN_SPEC, 'List the block patterns registered on WordPress by Loopress, the theme and plugins (not WordPress core). Only Loopress ones are pulled and pushed.', () => '');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPullCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPullCommand(PATTERN_SPEC, 'Pull the block patterns Loopress manages on WordPress into patterns/<slug>.html files. ' +
|
|
4
|
+
'Patterns registered by the theme or a plugin are never pulled, see `lps pattern list`.');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPushCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPushCommand(PATTERN_SPEC, 'Push local block patterns (patterns/<slug>.html, block markup under a title/categories header) to WordPress, ' +
|
|
4
|
+
'registered as loopress/<slug>. Create or update only, never deletes a pattern (use `lps pattern rm`).');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredRmCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredRmCommand(PATTERN_SPEC, 'Stop registering a Loopress block pattern on WordPress. A wp:pattern reference to it renders nothing, copies already inserted in posts stay.', 'A wp:pattern reference to it renders nothing until it is pushed again, copies already inserted in posts stay.');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('pattern', {
|
|
3
|
+
description: 'Restore block patterns to a snapshot taken automatically before an earlier `lps pattern push`',
|
|
4
|
+
pathNoun: 'patterns directory',
|
|
5
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
3
4
|
import { pinVersion } from '../../types/plugin.js';
|
|
4
5
|
import { compareVersions } from '../../utils/version.js';
|
|
5
6
|
const WPVULN_BASE = 'https://www.wpvulnerability.net/plugin';
|
|
@@ -19,7 +20,7 @@ export default class Audit extends LoopressCommand {
|
|
|
19
20
|
const advisories = [];
|
|
20
21
|
const health = [];
|
|
21
22
|
for (const slug of slugs) {
|
|
22
|
-
const found = await this.auditOne(slug, pinVersion(manifest[slug]));
|
|
23
|
+
const found = await withSpinner(`Auditing ${slug}`, async () => this.auditOne(slug, pinVersion(manifest[slug])));
|
|
23
24
|
advisories.push(...found.advisories);
|
|
24
25
|
health.push(...found.health);
|
|
25
26
|
}
|
|
@@ -4,14 +4,18 @@ import { type MergeResult } from '../../utils/plugins.js';
|
|
|
4
4
|
type PullResult = MergeResult & {
|
|
5
5
|
skipped?: ComposerPullResult['skipped'];
|
|
6
6
|
status: 'dry-run' | 'success';
|
|
7
|
+
untracked: string[];
|
|
7
8
|
};
|
|
8
9
|
export default class Pull extends LoopressCommand {
|
|
9
10
|
static description: string;
|
|
10
11
|
static enableJsonFlag: boolean;
|
|
11
12
|
static examples: string[];
|
|
12
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>;
|
|
13
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
17
|
};
|
|
15
18
|
run(): Promise<PullResult>;
|
|
19
|
+
private selectPlugins;
|
|
16
20
|
}
|
|
17
21
|
export {};
|
|
@@ -1,36 +1,60 @@
|
|
|
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';
|
|
4
|
-
import {
|
|
7
|
+
import { isInteractive } from '../../lib/interactive.js';
|
|
8
|
+
import { getComposerManagedSlugs, NOT_ON_WORDPRESS_ORG, pullIntoComposerJson, } from '../../utils/composer.js';
|
|
5
9
|
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
6
10
|
import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
7
11
|
export default class Pull extends LoopressCommand {
|
|
8
|
-
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.';
|
|
9
15
|
static enableJsonFlag = true;
|
|
10
|
-
static examples = ['$ lps plugin pull', '$ lps plugin pull --
|
|
16
|
+
static examples = ['$ lps plugin pull', '$ lps plugin pull --env production --plugin query-monitor'];
|
|
11
17
|
static flags = {
|
|
12
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
|
+
}),
|
|
13
24
|
};
|
|
14
25
|
async run() {
|
|
26
|
+
const { flags } = await this.parse(Pull);
|
|
15
27
|
const { url } = this.siteConfig;
|
|
16
28
|
this.log(`Pulling plugins from ${url}`);
|
|
17
29
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
18
|
-
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
|
+
}
|
|
19
42
|
// Pin every plugin to the version actually running on the site, and record the inactive ones
|
|
20
43
|
// as such so a later `plugin push` doesn't switch them on. A later `plugin push` installs
|
|
21
44
|
// exactly this set via Composer + WPackagist; drift only surfaces when the pinned version and
|
|
22
45
|
// the live version disagree.
|
|
23
46
|
const versions = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
|
|
24
|
-
const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
|
|
25
47
|
// A composer.json is authoritative over loopress.json: pin the live versions there instead.
|
|
26
|
-
|
|
27
|
-
if (existsSync(composerJsonPath)) {
|
|
48
|
+
if (composerJson) {
|
|
28
49
|
const result = await pullIntoComposerJson(composerJsonPath, 'plugin', versions, {
|
|
29
50
|
dryRun: this.dryRun,
|
|
30
51
|
log: this.log.bind(this),
|
|
31
52
|
});
|
|
32
|
-
|
|
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] };
|
|
33
56
|
}
|
|
57
|
+
const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
|
|
34
58
|
const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, pins);
|
|
35
59
|
if (this.dryRun) {
|
|
36
60
|
this.log(`[dry-run] Would write ${Object.keys(merged).length} plugins to loopress.json`);
|
|
@@ -38,7 +62,7 @@ export default class Pull extends LoopressCommand {
|
|
|
38
62
|
this.log(` + ${added.join(', ')}`);
|
|
39
63
|
for (const u of updated)
|
|
40
64
|
this.log(` ~ ${u.slug} (${u.from} → ${u.to})`);
|
|
41
|
-
return { added, merged, status: 'dry-run', updated };
|
|
65
|
+
return { added, merged, status: 'dry-run', untracked, updated };
|
|
42
66
|
}
|
|
43
67
|
await writeLocalConfig({ ...this.localConfig, plugins: merged });
|
|
44
68
|
this.log(`Wrote ${Object.keys(merged).length} plugins to loopress.json`);
|
|
@@ -46,6 +70,28 @@ export default class Pull extends LoopressCommand {
|
|
|
46
70
|
this.log(` + Added: ${added.join(', ')}`);
|
|
47
71
|
for (const u of updated)
|
|
48
72
|
this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
49
|
-
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;
|
|
50
96
|
}
|
|
51
97
|
}
|
|
@@ -4,9 +4,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { confirmUninstall } from '../../lib/interactive.js';
|
|
6
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
-
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
+
import { isLocalEnvironment, isNotFoundError } from '../../lib/wp-client.js';
|
|
8
8
|
import { pinActive, pinVersion, pinVersions, } from '../../types/plugin.js';
|
|
9
|
-
import { toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
9
|
+
import { 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';
|
|
@@ -178,11 +178,12 @@ export default class Push extends PushCommand {
|
|
|
178
178
|
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
179
179
|
if (existsSync(composerJsonPath)) {
|
|
180
180
|
const composerJson = JSON.parse(await readFile(composerJsonPath, 'utf8'));
|
|
181
|
-
const
|
|
181
|
+
const includeDev = isLocalEnvironment(this.siteConfig.name);
|
|
182
|
+
const manifest = wpackagistRequire(composerJson, 'plugin', includeDev);
|
|
182
183
|
if (Object.keys(manifest).length === 0) {
|
|
183
184
|
this.error('No wpackagist-plugin/* package in composer.json. Run `lps plugin pull` first, or `lps composer push` to push it as is.');
|
|
184
185
|
}
|
|
185
|
-
const require = composerJson
|
|
186
|
+
const require = requireToInstall(composerJson, includeDev);
|
|
186
187
|
return { file: 'composer.json', intent: toIntent(require), manifest, packageCount: Object.keys(require).length };
|
|
187
188
|
}
|
|
188
189
|
if (activate) {
|
|
@@ -4,9 +4,12 @@ import { configManager } from '../../config/project-config.manager.js';
|
|
|
4
4
|
import { bootstrapLoopressFull } from '../../lib/bootstrap-full-install.js';
|
|
5
5
|
import { isInteractive } from '../../lib/interactive.js';
|
|
6
6
|
import { isLoopressFullActive } from '../../lib/plugin-detection.js';
|
|
7
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
8
|
+
import { sweepTempAdmins } from '../../lib/temp-admin.js';
|
|
7
9
|
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
8
10
|
import { isLocalEnvironment, WpClient } from '../../lib/wp-client.js';
|
|
9
11
|
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
12
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
10
13
|
const NEW_PROJECT = '__new__';
|
|
11
14
|
const AUTH_BROWSER = 'browser';
|
|
12
15
|
const AUTH_MANUAL = 'manual';
|
|
@@ -96,6 +99,10 @@ export default class Config extends Command {
|
|
|
96
99
|
if (!token)
|
|
97
100
|
return; // env was just built with one a few lines up; guard only narrows the type
|
|
98
101
|
const wp = new WpClient(url, token, name);
|
|
102
|
+
// Before the "already active" early return: an install killed mid-way (SIGKILL, power loss)
|
|
103
|
+
// may have activated Full and still left its temp admin behind, and re-running this command
|
|
104
|
+
// is the documented fix for both.
|
|
105
|
+
await this.sweepLeftoverTempAdmins(wp);
|
|
99
106
|
// A detection failure (network hiccup, plugins REST disabled) is treated as "assume it's
|
|
100
107
|
// there" rather than surprising the user with an account-creating prompt they didn't ask
|
|
101
108
|
// for; `lps project config` can simply be re-run if it genuinely isn't installed.
|
|
@@ -109,7 +116,9 @@ export default class Config extends Command {
|
|
|
109
116
|
if (!isProceed)
|
|
110
117
|
return;
|
|
111
118
|
try {
|
|
112
|
-
await bootstrapLoopressFull(wp, url, (message) => {
|
|
119
|
+
await withSpinner('Installing Loopress Full', async () => bootstrapLoopressFull(wp, url, (message) => {
|
|
120
|
+
this.log(message);
|
|
121
|
+
}));
|
|
113
122
|
}
|
|
114
123
|
catch (error) {
|
|
115
124
|
this.warn(error.message);
|
|
@@ -181,4 +190,14 @@ export default class Config extends Command {
|
|
|
181
190
|
});
|
|
182
191
|
return { projectId: configManager.createProjectId(projectName.trim()), projectName: projectName.trim() };
|
|
183
192
|
}
|
|
193
|
+
async sweepLeftoverTempAdmins(wp) {
|
|
194
|
+
try {
|
|
195
|
+
const removed = await sweepTempAdmins(wp);
|
|
196
|
+
if (removed.length > 0)
|
|
197
|
+
this.log(`✓ Removed ${pluralize(removed.length, 'leftover temporary admin account')}: ${removed.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
this.warn(error.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
184
203
|
}
|
|
@@ -14,15 +14,15 @@ export default class List extends Command {
|
|
|
14
14
|
}
|
|
15
15
|
return projects.map((project) => this.logProject(project));
|
|
16
16
|
}
|
|
17
|
-
// Only name/url/
|
|
17
|
+
// Only name/url/isDefault are surfaced, deliberately: EnvironmentConfig also carries the
|
|
18
18
|
// WordPress application password token, which must never end up in --json output.
|
|
19
19
|
logEnvironment(env) {
|
|
20
|
-
const envMarker = c(env.
|
|
21
|
-
const envName = c(env.
|
|
20
|
+
const envMarker = c(env.isDefault ? 'cyan' : 'dim', '·');
|
|
21
|
+
const envName = c(env.isDefault ? 'cyan' : 'dim', env.name.padEnd(15));
|
|
22
22
|
const envUrl = c('dim', env.url);
|
|
23
|
-
const
|
|
24
|
-
this.log(` ${envMarker} ${envName} ${envUrl}${
|
|
25
|
-
return {
|
|
23
|
+
const defaultTag = env.isDefault ? ` ${c('cyan', '[default]')}` : '';
|
|
24
|
+
this.log(` ${envMarker} ${envName} ${envUrl}${defaultTag}`);
|
|
25
|
+
return { isDefault: env.isDefault, name: env.name, url: env.url };
|
|
26
26
|
}
|
|
27
27
|
logProject(project) {
|
|
28
28
|
const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
|
|
@@ -3,6 +3,7 @@ import { Listr } from 'listr2';
|
|
|
3
3
|
import { authManager } from '../../config/auth.manager.js';
|
|
4
4
|
import { configManager } from '../../config/project-config.manager.js';
|
|
5
5
|
import { ApiClient } from '../../lib/api-client.js';
|
|
6
|
+
import { withoutSpinner } from '../../lib/spinner.js';
|
|
6
7
|
import { pluralize } from '../../utils/pluralize.js';
|
|
7
8
|
export default class Pull extends Command {
|
|
8
9
|
static description = 'Pull projects and environments from your Loopress account that are not configured locally yet';
|
|
@@ -23,14 +24,16 @@ export default class Pull extends Command {
|
|
|
23
24
|
let projectCount = 0;
|
|
24
25
|
let environmentCount = 0;
|
|
25
26
|
if (newApiProjects.length > 0) {
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
await withoutSpinner(async () => {
|
|
28
|
+
await new Listr(newApiProjects.map((apiProject) => ({
|
|
29
|
+
task: (_ctx, task) => {
|
|
30
|
+
this.pullProject(apiProject, task);
|
|
31
|
+
projectCount++;
|
|
32
|
+
environmentCount += apiProject.environments.length;
|
|
33
|
+
},
|
|
34
|
+
title: `Pull project "${apiProject.name}" from the API`,
|
|
35
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
36
|
+
});
|
|
34
37
|
}
|
|
35
38
|
this.log(`\n✓ Pulled ${pluralize(projectCount, 'project')}, ${pluralize(environmentCount, 'environment')} from your Loopress account`);
|
|
36
39
|
}
|
|
@@ -6,6 +6,7 @@ import { configManager } from '../../config/project-config.manager.js';
|
|
|
6
6
|
import { ApiClient } from '../../lib/api-client.js';
|
|
7
7
|
import { LoopressCommand } from '../../lib/base.js';
|
|
8
8
|
import { isInteractive } from '../../lib/interactive.js';
|
|
9
|
+
import { withoutSpinner } from '../../lib/spinner.js';
|
|
9
10
|
import { pluralize } from '../../utils/pluralize.js';
|
|
10
11
|
import { toSlug } from '../../utils/to-slug.js';
|
|
11
12
|
export default class Push extends Command {
|
|
@@ -66,15 +67,17 @@ export default class Push extends Command {
|
|
|
66
67
|
if (envsNeedingWork.length === 0)
|
|
67
68
|
return 0;
|
|
68
69
|
let count = 0;
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
await withoutSpinner(async () => {
|
|
71
|
+
await new Listr(envsNeedingWork.map(({ apiProjectId, envPlan, projectName }) => ({
|
|
72
|
+
task: async (_ctx, task) => {
|
|
73
|
+
await this.applyEnvironment(api, apiProjectId, envPlan, task);
|
|
74
|
+
count++;
|
|
75
|
+
},
|
|
76
|
+
title: envPlan.action === 'create'
|
|
77
|
+
? `Create environment "${envPlan.env.name}" on "${projectName}"`
|
|
78
|
+
: `Link environment "${envPlan.env.name}" on "${projectName}"`,
|
|
79
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
80
|
+
});
|
|
78
81
|
return count;
|
|
79
82
|
}
|
|
80
83
|
async applyProject(api, plan, task) {
|
|
@@ -101,15 +104,17 @@ export default class Push extends Command {
|
|
|
101
104
|
if (projectsNeedingWork.length === 0)
|
|
102
105
|
return 0;
|
|
103
106
|
let count = 0;
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
await withoutSpinner(async () => {
|
|
108
|
+
await new Listr(projectsNeedingWork.map((plan) => ({
|
|
109
|
+
task: async (_ctx, task) => {
|
|
110
|
+
await this.applyProject(api, plan, task);
|
|
111
|
+
count++;
|
|
112
|
+
},
|
|
113
|
+
title: plan.action === 'create'
|
|
114
|
+
? `Create project "${plan.project.name}" on the API`
|
|
115
|
+
: `Link project "${plan.project.name}" to the API`,
|
|
116
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
117
|
+
});
|
|
113
118
|
return count;
|
|
114
119
|
}
|
|
115
120
|
collectEnvsNeedingWork(projectPlans, envPlansByProject) {
|
|
@@ -24,7 +24,7 @@ export default class Remove extends Command {
|
|
|
24
24
|
const envChoices = configManager.listEnvironments(project.id).map((env) => {
|
|
25
25
|
targets.push({ env: env.name, kind: 'env', projectId: project.id, projectName: project.name });
|
|
26
26
|
return {
|
|
27
|
-
name: ` ${env.name.padEnd(20)} ${env.url}
|
|
27
|
+
name: ` ${env.name.padEnd(20)} ${env.url}`,
|
|
28
28
|
value: String(targets.length - 1),
|
|
29
29
|
};
|
|
30
30
|
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { select
|
|
2
|
-
import { Command
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
-
const c = ux.colorize;
|
|
5
4
|
export default class Switch extends Command {
|
|
6
|
-
static description = 'Switch the active project
|
|
5
|
+
static description = 'Switch the active project (pass --env to target a non-local environment)';
|
|
7
6
|
static examples = ['$ lps project switch'];
|
|
8
7
|
async run() {
|
|
9
8
|
await this.parse(Switch);
|
|
@@ -11,37 +10,18 @@ export default class Switch extends Command {
|
|
|
11
10
|
if (projects.length === 0) {
|
|
12
11
|
this.error('No projects configured. Run `lps project config` first.');
|
|
13
12
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const { env, project } = entries[0];
|
|
28
|
-
return { envName: env.name, projectId: project.id, projectName: project.name };
|
|
29
|
-
}
|
|
30
|
-
const choices = groups.flatMap(({ envs, project }) => [
|
|
31
|
-
new Separator(c(project.isCurrent ? 'green' : 'dim', `─── ${project.name} ───`)),
|
|
32
|
-
...envs.map((env) => ({
|
|
33
|
-
name: `${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
34
|
-
value: `${project.id}::${env.name}`,
|
|
35
|
-
})),
|
|
36
|
-
]);
|
|
37
|
-
const current = entries.find(({ env }) => env.isCurrent);
|
|
38
|
-
const chosen = await select({
|
|
39
|
-
choices,
|
|
40
|
-
default: current && `${current.project.id}::${current.env.name}`,
|
|
41
|
-
message: 'Select project / environment',
|
|
42
|
-
});
|
|
43
|
-
const [projectId, envName] = chosen.split('::', 2);
|
|
44
|
-
const { project } = groups.find((group) => group.project.id === projectId);
|
|
45
|
-
return { envName, projectId, projectName: project.name };
|
|
13
|
+
const projectId = projects.length === 1
|
|
14
|
+
? projects[0].id
|
|
15
|
+
: await select({
|
|
16
|
+
choices: projects.map((project) => ({
|
|
17
|
+
name: `${project.name}${project.isCurrent ? ' [current]' : ''}`,
|
|
18
|
+
value: project.id,
|
|
19
|
+
})),
|
|
20
|
+
default: projects.find((project) => project.isCurrent)?.id,
|
|
21
|
+
message: 'Select project',
|
|
22
|
+
});
|
|
23
|
+
configManager.setCurrent(projectId);
|
|
24
|
+
const project = projects.find((candidate) => candidate.id === projectId);
|
|
25
|
+
this.log(`✓ Switched to "${project.name}"`);
|
|
46
26
|
}
|
|
47
27
|
}
|