@loopress/cli 0.22.0 → 0.24.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 +794 -85
- package/dist/commands/acf/diff.d.ts +2 -0
- package/dist/commands/acf/diff.js +5 -0
- package/dist/commands/api/diff.d.ts +2 -0
- package/dist/commands/api/diff.js +5 -0
- package/dist/commands/api/list.d.ts +2 -12
- package/dist/commands/api/list.js +2 -20
- package/dist/commands/api/pull.d.ts +2 -20
- package/dist/commands/api/pull.js +2 -53
- package/dist/commands/api/push.d.ts +2 -22
- package/dist/commands/api/push.js +2 -116
- package/dist/commands/app/list.d.ts +17 -0
- package/dist/commands/app/list.js +35 -0
- package/dist/commands/{page → app}/pull.d.ts +7 -5
- package/dist/commands/app/pull.js +94 -0
- package/dist/commands/app/push.d.ts +28 -0
- package/dist/commands/app/push.js +130 -0
- package/dist/commands/app/remove.d.ts +20 -0
- package/dist/commands/app/remove.js +42 -0
- package/dist/commands/composer/pull.js +6 -16
- package/dist/commands/composer/push.d.ts +1 -0
- package/dist/commands/composer/push.js +43 -19
- package/dist/commands/dev.js +1 -1
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +42 -0
- package/dist/commands/form/diff.d.ts +2 -0
- package/dist/commands/form/diff.js +5 -0
- package/dist/commands/hook/diff.d.ts +2 -0
- package/dist/commands/hook/diff.js +5 -0
- package/dist/commands/hook/list.d.ts +2 -0
- package/dist/commands/hook/list.js +2 -0
- package/dist/commands/hook/pull.d.ts +2 -0
- package/dist/commands/hook/pull.js +2 -0
- package/dist/commands/hook/push.d.ts +2 -0
- package/dist/commands/hook/push.js +2 -0
- package/dist/commands/init.js +2 -1
- package/dist/commands/option/add.d.ts +15 -0
- package/dist/commands/option/add.js +47 -0
- package/dist/commands/option/diff.d.ts +2 -0
- package/dist/commands/option/diff.js +5 -0
- package/dist/commands/{page → option}/list.d.ts +5 -1
- package/dist/commands/option/list.js +151 -0
- package/dist/commands/option/pull.d.ts +16 -0
- package/dist/commands/option/pull.js +75 -0
- package/dist/commands/{page → option}/push.d.ts +3 -8
- package/dist/commands/option/push.js +66 -0
- package/dist/commands/option/remove.d.ts +22 -0
- package/dist/commands/option/remove.js +69 -0
- package/dist/commands/plugin/add.d.ts +1 -0
- package/dist/commands/plugin/add.js +18 -8
- package/dist/commands/plugin/audit.d.ts +30 -0
- package/dist/commands/plugin/audit.js +138 -0
- package/dist/commands/plugin/pull.d.ts +1 -2
- package/dist/commands/plugin/pull.js +11 -11
- package/dist/commands/plugin/push.d.ts +13 -8
- package/dist/commands/plugin/push.js +161 -67
- package/dist/commands/plugin/status.d.ts +22 -0
- package/dist/commands/plugin/status.js +65 -0
- package/dist/commands/pull.js +3 -2
- package/dist/commands/push.js +4 -3
- package/dist/commands/seo/diff.d.ts +2 -0
- package/dist/commands/seo/diff.js +5 -0
- package/dist/commands/snippet/diff.d.ts +2 -0
- package/dist/commands/snippet/diff.js +5 -0
- package/dist/commands/theme/add.d.ts +13 -0
- package/dist/commands/theme/add.js +35 -0
- package/dist/commands/theme/pull.d.ts +21 -0
- package/dist/commands/theme/pull.js +37 -0
- package/dist/commands/theme/push.d.ts +22 -0
- package/dist/commands/theme/push.js +123 -0
- package/dist/commands/theme/status.d.ts +20 -0
- package/dist/commands/theme/status.js +51 -0
- package/dist/lib/app-manifest.d.ts +36 -0
- package/dist/lib/app-manifest.js +162 -0
- package/dist/lib/base.d.ts +3 -1
- package/dist/lib/base.js +14 -19
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +1 -2
- package/dist/lib/diff-command.d.ts +47 -0
- package/dist/lib/diff-command.js +180 -0
- package/dist/lib/diff-state.d.ts +22 -0
- package/dist/lib/diff-state.js +88 -0
- package/dist/lib/find-orphaned-files.js +1 -1
- package/dist/lib/interactive.d.ts +1 -0
- package/dist/lib/interactive.js +10 -0
- package/dist/lib/php-files-command.d.ts +54 -0
- package/dist/lib/php-files-command.js +216 -0
- package/dist/lib/put-or-create.js +1 -1
- package/dist/lib/readdir-tolerant.d.ts +1 -0
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.d.ts +14 -0
- package/dist/lib/resource-state.js +361 -0
- package/dist/lib/validate-local.js +15 -7
- package/dist/lib/wp-client.d.ts +5 -0
- package/dist/lib/wp-client.js +56 -0
- package/dist/types/plugin.d.ts +1 -0
- package/dist/types/project-config.generated.d.ts +19 -5
- package/dist/utils/option-format.d.ts +31 -0
- package/dist/utils/option-format.js +51 -0
- package/dist/utils/plugin-sync.d.ts +26 -0
- package/dist/utils/plugin-sync.js +31 -0
- package/dist/utils/plugins.d.ts +15 -2
- package/dist/utils/plugins.js +57 -12
- package/dist/utils/resource-dirs.d.ts +13 -0
- package/dist/utils/resource-dirs.js +34 -0
- package/dist/utils/themes.d.ts +30 -0
- package/dist/utils/themes.js +29 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.js +21 -0
- package/oclif.manifest.json +1897 -244
- package/package.json +26 -19
- package/schemas/project-config.schema.json +32 -9
- package/dist/commands/page/list.js +0 -19
- package/dist/commands/page/pull.js +0 -57
- package/dist/commands/page/push.js +0 -132
- package/dist/utils/page-format.d.ts +0 -7
- package/dist/utils/page-format.js +0 -78
|
@@ -1,98 +1,192 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { confirmUninstall } from '../../lib/interactive.js';
|
|
2
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
+
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS } from '../../utils/plugin-sync.js';
|
|
8
|
+
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
9
|
+
const IN_SYNC = { activated: [], installed: [], pinned: [], pruned: [], removed: [], status: 'in-sync' };
|
|
6
10
|
export default class Push extends PushCommand {
|
|
7
|
-
static description = '
|
|
11
|
+
static description = 'Install plugins on WordPress to match loopress.json, pinned via Composer + WPackagist';
|
|
8
12
|
static enableJsonFlag = true;
|
|
9
|
-
static examples = ['$ lps plugin push', '$ lps plugin push --dry-run'];
|
|
13
|
+
static examples = ['$ lps plugin push', '$ lps plugin push --dry-run', '$ lps plugin push --force --prune'];
|
|
10
14
|
static flags = {
|
|
11
15
|
...PushCommand.dryRunFlag,
|
|
12
16
|
...PushCommand.yesFlag,
|
|
17
|
+
force: Flags.boolean({
|
|
18
|
+
default: false,
|
|
19
|
+
description: 'Allow downgrades and let Loopress take over plugins installed outside it (replaces their files)',
|
|
20
|
+
}),
|
|
21
|
+
prune: Flags.boolean({
|
|
22
|
+
default: false,
|
|
23
|
+
description: 'Deactivate plugins that are active on the site but absent from loopress.json',
|
|
24
|
+
}),
|
|
13
25
|
};
|
|
14
26
|
async run() {
|
|
15
|
-
const {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
this.
|
|
27
|
+
const { flags } = await this.parse(Push);
|
|
28
|
+
const { force, prune } = flags;
|
|
29
|
+
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
30
|
+
this.warn('This project has a composer.json, which is authoritative for plugins. Run `lps composer push` instead.');
|
|
31
|
+
return { ...IN_SYNC, status: 'composer-managed' };
|
|
19
32
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
this.log(`Pushing plugins to ${url}`);
|
|
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
|
+
}
|
|
37
|
+
this.log(`Pushing plugins to ${this.siteConfig.url}`);
|
|
26
38
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
27
39
|
const installed = parseInstalledPlugins(raw);
|
|
28
|
-
const
|
|
29
|
-
|
|
40
|
+
const managed = lockedWpackagistSlugs(await this.fetchInstanceLock(), 'plugin');
|
|
41
|
+
const diff = diffPlugins(manifest, installed, managed);
|
|
42
|
+
this.guardForce(diff, force);
|
|
43
|
+
const toPrune = prune ? diff.untrackedActive : [];
|
|
44
|
+
// A "latest" pin never shows as drift, but its newest upstream release may have moved
|
|
45
|
+
// since the last push, so a push must still run `composer update` to pick it up.
|
|
46
|
+
const hasLatestPin = Object.values(manifest).includes('latest');
|
|
47
|
+
if (!hasLatestPin && isNoop(diff, toPrune)) {
|
|
30
48
|
this.log('Everything is already in sync.');
|
|
31
|
-
return {
|
|
49
|
+
return { ...IN_SYNC };
|
|
50
|
+
}
|
|
51
|
+
if (isNoop(diff, toPrune))
|
|
52
|
+
this.log('Refreshing plugins pinned to "latest" to their newest releases.');
|
|
53
|
+
logPlan(this, diff, toPrune, force);
|
|
54
|
+
if (this.dryRun) {
|
|
55
|
+
return this.result(diff, { includeCollisions: true, pruned: toPrune, removed: diff.toRemove, status: 'dry-run' });
|
|
32
56
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
if (!(await confirmUninstall(diff.toRemove, this.yes)))
|
|
58
|
+
this.error('Aborted.');
|
|
59
|
+
const deactivated = await this.deactivateEndangered(installed, diff, force);
|
|
60
|
+
// Plugins that must end up active: the ones we deactivated for the file swap (minus any
|
|
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];
|
|
63
|
+
let response;
|
|
64
|
+
try {
|
|
65
|
+
response = await this.sync(manifest, force);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// The file swap never happened, so restore the plugins we defensively deactivated
|
|
69
|
+
// instead of leaving the site with them switched off.
|
|
70
|
+
await this.activate(deactivated.filter((p) => !diff.toRemove.includes(p.slug)));
|
|
71
|
+
throw error;
|
|
43
72
|
}
|
|
73
|
+
if (toPrune.length > 0) {
|
|
74
|
+
await this.deactivate(installed.filter((p) => toPrune.includes(p.slug)));
|
|
75
|
+
}
|
|
76
|
+
await this.activate(toReactivate);
|
|
77
|
+
if (response.output.trim())
|
|
78
|
+
this.log(response.output.trim());
|
|
79
|
+
this.log('Plugins synced.');
|
|
44
80
|
await this.recordSuccess();
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
return this.result(diff, {
|
|
82
|
+
activated: toReactivate.map((p) => p.slug),
|
|
83
|
+
includeCollisions: force,
|
|
84
|
+
pruned: toPrune,
|
|
85
|
+
removed: response.removed ?? diff.toRemove,
|
|
86
|
+
status: 'success',
|
|
87
|
+
});
|
|
49
88
|
}
|
|
50
|
-
async
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
title: `Install ${action.slug}`,
|
|
55
|
-
})), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
89
|
+
async activate(plugins) {
|
|
90
|
+
for (const plugin of plugins) {
|
|
91
|
+
this.log(` ⊙ activating ${plugin.slug}`);
|
|
92
|
+
await this.wp.put(`wp/v2/plugins/${plugin.file}`, { status: 'active' });
|
|
56
93
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
}
|
|
95
|
+
async deactivate(plugins) {
|
|
96
|
+
for (const plugin of plugins) {
|
|
97
|
+
this.log(` ⊘ deactivating ${plugin.slug}`);
|
|
98
|
+
await this.wp.put(`wp/v2/plugins/${plugin.file}`, { status: 'inactive' });
|
|
62
99
|
}
|
|
63
100
|
}
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
// Deactivate anything whose folder is about to be deleted or replaced (removed, re-pinned to
|
|
102
|
+
// a new version, or taken over with --force), so the site does not fatal in the window
|
|
103
|
+
// between removal and Composer finishing the install. Returns the plugins it switched off so
|
|
104
|
+
// the caller can switch the still-wanted ones back on.
|
|
105
|
+
async deactivateEndangered(installed, diff, force) {
|
|
106
|
+
const endangered = new Set([
|
|
107
|
+
...diff.toRemove,
|
|
108
|
+
...diff.toPin.map((p) => p.slug),
|
|
109
|
+
...(force ? diff.collisions.map((c) => c.slug) : []),
|
|
110
|
+
]);
|
|
111
|
+
const victims = installed.filter((p) => p.active && endangered.has(p.slug));
|
|
112
|
+
await this.deactivate(victims);
|
|
113
|
+
return victims;
|
|
66
114
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
115
|
+
// The site's live composer.lock tells us which plugins Loopress already manages, so the plan
|
|
116
|
+
// can preview removals and tell an unmanaged folder (collision) apart from a re-pin. 404 =
|
|
117
|
+
// nothing pushed yet.
|
|
118
|
+
async fetchInstanceLock() {
|
|
119
|
+
try {
|
|
120
|
+
const { composerLock } = await this.wp.get('loopress/v1/composer/lock');
|
|
121
|
+
return composerLock;
|
|
72
122
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (isNotFoundError(error))
|
|
125
|
+
return null;
|
|
126
|
+
throw error;
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
|
-
|
|
80
|
-
if (
|
|
129
|
+
guardForce(diff, force) {
|
|
130
|
+
if (force)
|
|
81
131
|
return;
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
if (diff.collisions.length > 0) {
|
|
133
|
+
const list = diff.collisions.map((c) => `${c.slug} (${c.installedVersion})`).join(', ');
|
|
134
|
+
this.error(`${diff.collisions.length} plugin(s) are already installed outside Loopress: ${list}. ` +
|
|
135
|
+
'Re-run with --force to let Loopress manage them (this replaces their files with the WPackagist build, ' +
|
|
136
|
+
'local modifications are lost), or remove them from loopress.json.');
|
|
137
|
+
}
|
|
138
|
+
const downgrades = diff.toPin.filter((p) => isDowngrade(p.from, p.to));
|
|
139
|
+
if (downgrades.length > 0) {
|
|
140
|
+
const list = downgrades.map((p) => `${p.slug} ${p.from} to ${p.to}`).join(', ');
|
|
141
|
+
this.error(`Refusing to downgrade: ${list}. A downgrade only replaces plugin files, it does not undo database ` +
|
|
142
|
+
'migrations the newer version ran, which can break the site. Re-run with --force, or bump the version in loopress.json.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
result(diff, opts) {
|
|
146
|
+
const collisionSlugs = opts.includeCollisions ? diff.collisions.map((c) => c.slug) : [];
|
|
147
|
+
return {
|
|
148
|
+
activated: opts.activated ?? diff.toActivate.map((a) => a.slug),
|
|
149
|
+
installed: [...diff.toInstall.map((a) => a.slug), ...collisionSlugs],
|
|
150
|
+
pinned: diff.toPin.map((p) => p.slug),
|
|
151
|
+
pruned: opts.pruned,
|
|
152
|
+
removed: opts.removed,
|
|
153
|
+
status: opts.status,
|
|
154
|
+
};
|
|
84
155
|
}
|
|
85
|
-
async
|
|
156
|
+
async sync(manifest, force) {
|
|
86
157
|
try {
|
|
87
|
-
await
|
|
88
|
-
const message = `✓ ${slug} ${verb === 'install' ? 'installed and activated' : 'activated'}`;
|
|
89
|
-
if (task)
|
|
90
|
-
task.output = message;
|
|
91
|
-
else
|
|
92
|
-
this.log(` ${message}`);
|
|
158
|
+
return await this.wp.post('loopress/v1/composer/sync', { force, intent: { plugins: manifest }, lock: null }, { timeoutMs: SYNC_TIMEOUT_MS });
|
|
93
159
|
}
|
|
94
160
|
catch (error) {
|
|
95
|
-
|
|
161
|
+
const collisions = parseCollisions(error);
|
|
162
|
+
if (collisions) {
|
|
163
|
+
this.error(`The site rejected the push: ${collisions.map((c) => c.slug).join(', ')} installed outside Loopress. Re-run with --force.`);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
96
166
|
}
|
|
97
167
|
}
|
|
98
168
|
}
|
|
169
|
+
function isNoop(diff, toPrune) {
|
|
170
|
+
return (diff.toInstall.length === 0 &&
|
|
171
|
+
diff.toPin.length === 0 &&
|
|
172
|
+
diff.toActivate.length === 0 &&
|
|
173
|
+
diff.toRemove.length === 0 &&
|
|
174
|
+
diff.collisions.length === 0 &&
|
|
175
|
+
toPrune.length === 0);
|
|
176
|
+
}
|
|
177
|
+
function logPlan(cmd, diff, prune, force) {
|
|
178
|
+
const section = (label, lines) => {
|
|
179
|
+
if (lines.length === 0)
|
|
180
|
+
return;
|
|
181
|
+
cmd.log(`\n${label} (${lines.length}):`);
|
|
182
|
+
for (const line of lines)
|
|
183
|
+
cmd.log(` ${line}`);
|
|
184
|
+
};
|
|
185
|
+
section('To install', diff.toInstall.map((a) => `+ ${a.slug} ${a.version}`));
|
|
186
|
+
if (force)
|
|
187
|
+
section('To take over', diff.collisions.map((c) => `! ${c.slug} (installed outside Loopress)`));
|
|
188
|
+
section('To re-pin', diff.toPin.map((p) => `~ ${p.slug} ${p.from} to ${p.to}`));
|
|
189
|
+
section('To activate', diff.toActivate.map((a) => `↑ ${a.slug}`));
|
|
190
|
+
section('To uninstall', diff.toRemove.map((s) => `- ${s}`));
|
|
191
|
+
section('To deactivate (--prune)', prune.map((s) => `⊘ ${s}`));
|
|
192
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type StatusResult = {
|
|
3
|
+
drift: boolean;
|
|
4
|
+
inactive: string[];
|
|
5
|
+
missing: string[];
|
|
6
|
+
status: 'composer-managed' | 'drift' | 'in-sync';
|
|
7
|
+
untrackedActive: string[];
|
|
8
|
+
wrongVersion: Array<{
|
|
9
|
+
live: string;
|
|
10
|
+
pinned: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export default class Status extends LoopressCommand {
|
|
15
|
+
static description: string;
|
|
16
|
+
static enableJsonFlag: boolean;
|
|
17
|
+
static examples: string[];
|
|
18
|
+
static flags: {};
|
|
19
|
+
run(): Promise<StatusResult>;
|
|
20
|
+
private fetchInstanceLock;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
5
|
+
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
6
|
+
export default class Status extends LoopressCommand {
|
|
7
|
+
static description = 'Compare the plugins on WordPress against loopress.json and report drift';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = ['$ lps plugin status'];
|
|
10
|
+
static flags = {};
|
|
11
|
+
async run() {
|
|
12
|
+
const { url } = this.siteConfig;
|
|
13
|
+
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
14
|
+
this.warn('This project has a composer.json, which is authoritative for plugins. Run `lps composer` commands instead.');
|
|
15
|
+
return { drift: false, inactive: [], missing: [], status: 'composer-managed', untrackedActive: [], wrongVersion: [] };
|
|
16
|
+
}
|
|
17
|
+
const manifest = this.localConfig.plugins ?? {};
|
|
18
|
+
const raw = await this.wp.get('wp/v2/plugins');
|
|
19
|
+
const installed = parseInstalledPlugins(raw);
|
|
20
|
+
const managed = lockedWpackagistSlugs(await this.fetchInstanceLock(), 'plugin');
|
|
21
|
+
const diff = diffPlugins(manifest, installed, managed);
|
|
22
|
+
const missing = diff.toInstall.map((a) => a.slug);
|
|
23
|
+
const wrongVersion = diff.toPin.map((p) => ({ live: p.from, pinned: p.to, slug: p.slug }));
|
|
24
|
+
const inactive = diff.toActivate.map((a) => a.slug);
|
|
25
|
+
const { untrackedActive } = diff;
|
|
26
|
+
// `collisions` are also drift (Loopress doesn't own them yet); surface them under missing's
|
|
27
|
+
// banner so `plugin push --force` is the obvious next step.
|
|
28
|
+
const collisions = diff.collisions.map((c) => c.slug);
|
|
29
|
+
const drift = missing.length > 0 ||
|
|
30
|
+
wrongVersion.length > 0 ||
|
|
31
|
+
inactive.length > 0 ||
|
|
32
|
+
diff.toRemove.length > 0 ||
|
|
33
|
+
collisions.length > 0;
|
|
34
|
+
if (!drift && untrackedActive.length === 0) {
|
|
35
|
+
this.log(`In sync with loopress.json (${url})`);
|
|
36
|
+
return { drift: false, inactive: [], missing: [], status: 'in-sync', untrackedActive: [], wrongVersion: [] };
|
|
37
|
+
}
|
|
38
|
+
if (missing.length > 0)
|
|
39
|
+
this.log(`Not installed: ${missing.join(', ')}`);
|
|
40
|
+
if (collisions.length > 0)
|
|
41
|
+
this.log(`Installed outside Loopress (run \`plugin push --force\`): ${collisions.join(', ')}`);
|
|
42
|
+
for (const w of wrongVersion)
|
|
43
|
+
this.log(`Version drift: ${w.slug} is ${w.live}, loopress.json pins ${w.pinned}`);
|
|
44
|
+
if (inactive.length > 0)
|
|
45
|
+
this.log(`Pinned but inactive: ${inactive.join(', ')}`);
|
|
46
|
+
if (diff.toRemove.length > 0)
|
|
47
|
+
this.log(`Managed by Loopress but dropped from loopress.json: ${diff.toRemove.join(', ')}`);
|
|
48
|
+
if (untrackedActive.length > 0)
|
|
49
|
+
this.log(`Active but untracked: ${untrackedActive.join(', ')}`);
|
|
50
|
+
if (drift)
|
|
51
|
+
this.exit(1);
|
|
52
|
+
return { drift, inactive, missing, status: drift ? 'drift' : 'in-sync', untrackedActive, wrongVersion };
|
|
53
|
+
}
|
|
54
|
+
async fetchInstanceLock() {
|
|
55
|
+
try {
|
|
56
|
+
const { composerLock } = await this.wp.get('loopress/v1/composer/lock');
|
|
57
|
+
return composerLock;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isNotFoundError(error))
|
|
61
|
+
return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/commands/pull.js
CHANGED
|
@@ -8,13 +8,14 @@ const PULL_TARGETS = [
|
|
|
8
8
|
{ commandId: 'plugin:pull', label: 'plugins' },
|
|
9
9
|
{ commandId: 'acf:pull', label: 'ACF', supportsYes: true },
|
|
10
10
|
{ commandId: 'api:pull', label: 'API routes', supportsYes: true },
|
|
11
|
+
{ commandId: 'hook:pull', label: 'hooks', supportsYes: true },
|
|
11
12
|
{ commandId: 'form:pull', label: 'forms', supportsYes: true },
|
|
12
|
-
{ commandId: 'page:pull', label: 'pages', supportsYes: true },
|
|
13
13
|
{ commandId: 'seo:pull', label: 'SEO', supportsYes: true },
|
|
14
|
+
{ commandId: 'option:pull', label: 'options', supportsYes: true },
|
|
14
15
|
{ commandId: 'snippet:pull', label: 'snippets', supportsYes: true },
|
|
15
16
|
];
|
|
16
17
|
export default class Pull extends LoopressCommand {
|
|
17
|
-
static description = 'Pull all content, plugins, composer dependencies, ACF, API routes,
|
|
18
|
+
static description = 'Pull all content, plugins, composer dependencies, ACF, API routes, hooks, forms, SEO, options, and snippets, from WordPress';
|
|
18
19
|
static examples = ['$ lps pull', '$ lps pull --env staging', '$ lps pull --dry-run'];
|
|
19
20
|
static flags = {
|
|
20
21
|
...LoopressCommand.dryRunFlag,
|
package/dist/commands/push.js
CHANGED
|
@@ -8,13 +8,14 @@ const PUSH_TARGETS = [
|
|
|
8
8
|
{ commandId: 'composer:push', label: 'composer' },
|
|
9
9
|
{ commandId: 'acf:push', label: 'ACF' },
|
|
10
10
|
{ commandId: 'api:push', label: 'API routes' },
|
|
11
|
+
{ commandId: 'hook:push', label: 'hooks' },
|
|
11
12
|
{ commandId: 'form:push', label: 'forms' },
|
|
12
|
-
{ commandId: 'page:push', label: 'pages' },
|
|
13
13
|
{ commandId: 'seo:push', label: 'SEO' },
|
|
14
|
+
{ commandId: 'option:push', label: 'options' },
|
|
14
15
|
{ commandId: 'snippet:push', label: 'snippets' },
|
|
15
16
|
];
|
|
16
17
|
export default class Push extends LoopressCommand {
|
|
17
|
-
static description = 'Push all local content, plugins, composer dependencies, ACF, API routes,
|
|
18
|
+
static description = 'Push all local content, plugins, composer dependencies, ACF, API routes, hooks, forms, SEO, options, and snippets, to WordPress';
|
|
18
19
|
static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run'];
|
|
19
20
|
static flags = {
|
|
20
21
|
...LoopressCommand.dryRunFlag,
|
|
@@ -42,7 +43,7 @@ export default class Push extends LoopressCommand {
|
|
|
42
43
|
}
|
|
43
44
|
// Every delegated push always gets --yes: either this command's own guard already confirmed
|
|
44
45
|
// production once above, or the target isn't production and there's nothing to confirm.
|
|
45
|
-
// Without this, each
|
|
46
|
+
// Without this, each delegated command would re-run its own production guard.
|
|
46
47
|
buildArgv() {
|
|
47
48
|
const argv = ['--env', this.siteConfig.name, '--yes'];
|
|
48
49
|
if (this.dryRun)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('seo', {
|
|
3
|
+
description: 'Show what differs in SEO settings, post meta, and redirects between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'SEO directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('snippet', {
|
|
3
|
+
description: 'Show what differs in snippets between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'snippets directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
export default class Add extends LoopressCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
version: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
4
|
+
import { isExactVersion } from '../../utils/version.js';
|
|
5
|
+
export default class Add extends LoopressCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
slug: Args.string({ description: 'Theme slug on WordPress.org', required: true }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Add a WordPress.org theme to loopress.json';
|
|
10
|
+
static examples = ['$ lps theme add generatepress', '$ lps theme add generatepress --version 3.4.0'];
|
|
11
|
+
static flags = {
|
|
12
|
+
...LoopressCommand.dryRunFlag,
|
|
13
|
+
version: Flags.string({ description: 'Exact version to pin (default: "latest")' }),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(Add);
|
|
17
|
+
const { slug } = args;
|
|
18
|
+
const version = flags.version ?? 'latest';
|
|
19
|
+
if (version !== 'latest' && !isExactVersion(version)) {
|
|
20
|
+
this.error(`--version must be an exact version like 3.4.0, not a Composer constraint. Got "${version}".`);
|
|
21
|
+
}
|
|
22
|
+
const existing = this.localConfig.themes ?? {};
|
|
23
|
+
if (existing[slug] === version) {
|
|
24
|
+
this.log(`${slug} is already pinned to ${version}, nothing to do.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const isUpdated = existing[slug] !== undefined;
|
|
28
|
+
if (this.dryRun) {
|
|
29
|
+
this.log(`[dry-run] Would ${isUpdated ? 'update' : 'add'} ${slug} (${version})`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await writeLocalConfig({ ...this.localConfig, themes: { ...existing, [slug]: version } });
|
|
33
|
+
this.log(`${isUpdated ? 'Updated' : 'Added'} ${slug} (${version}). Run \`lps theme push\` to apply.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type PullResult = {
|
|
3
|
+
added: string[];
|
|
4
|
+
merged: Record<string, string>;
|
|
5
|
+
status: 'composer-managed' | 'dry-run' | 'success';
|
|
6
|
+
updated: Array<{
|
|
7
|
+
from: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
to: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export default class Pull extends LoopressCommand {
|
|
13
|
+
static description: string;
|
|
14
|
+
static enableJsonFlag: boolean;
|
|
15
|
+
static examples: string[];
|
|
16
|
+
static flags: {
|
|
17
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<PullResult>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
+
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
5
|
+
import { mergePluginManifest } from '../../utils/plugins.js';
|
|
6
|
+
import { parseInstalledThemes } from '../../utils/themes.js';
|
|
7
|
+
export default class Pull extends LoopressCommand {
|
|
8
|
+
static description = 'Pull installed themes from WordPress into loopress.json, pinned to their live versions';
|
|
9
|
+
static enableJsonFlag = true;
|
|
10
|
+
static examples = ['$ lps theme pull'];
|
|
11
|
+
static flags = {
|
|
12
|
+
...LoopressCommand.dryRunFlag,
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { url } = this.siteConfig;
|
|
16
|
+
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
17
|
+
this.warn('This project has a composer.json, which is authoritative. Run `lps composer pull` instead.');
|
|
18
|
+
return { added: [], merged: this.localConfig.themes ?? {}, status: 'composer-managed', updated: [] };
|
|
19
|
+
}
|
|
20
|
+
this.log(`Pulling themes from ${url}`);
|
|
21
|
+
const raw = await this.wp.get('wp/v2/themes');
|
|
22
|
+
const installed = parseInstalledThemes(raw);
|
|
23
|
+
const incoming = Object.fromEntries(installed.map((t) => [t.slug, t.version]));
|
|
24
|
+
const { added, merged, updated } = mergePluginManifest(this.localConfig.themes ?? {}, incoming);
|
|
25
|
+
if (this.dryRun) {
|
|
26
|
+
this.log(`[dry-run] Would write ${Object.keys(merged).length} themes to loopress.json`);
|
|
27
|
+
return { added, merged, status: 'dry-run', updated };
|
|
28
|
+
}
|
|
29
|
+
await writeLocalConfig({ ...this.localConfig, themes: merged });
|
|
30
|
+
this.log(`Wrote ${Object.keys(merged).length} themes to loopress.json`);
|
|
31
|
+
if (added.length > 0)
|
|
32
|
+
this.log(` + Added: ${added.join(', ')}`);
|
|
33
|
+
for (const u of updated)
|
|
34
|
+
this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
35
|
+
return { added, merged, status: 'success', updated };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type PushResult = {
|
|
3
|
+
installed: string[];
|
|
4
|
+
pinned: string[];
|
|
5
|
+
removed: string[];
|
|
6
|
+
status: 'composer-managed' | 'dry-run' | 'in-sync' | 'success';
|
|
7
|
+
};
|
|
8
|
+
export default class Push extends PushCommand {
|
|
9
|
+
static description: string;
|
|
10
|
+
static enableJsonFlag: boolean;
|
|
11
|
+
static examples: string[];
|
|
12
|
+
static flags: {
|
|
13
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<PushResult>;
|
|
18
|
+
private fetchInstanceLock;
|
|
19
|
+
private guardForce;
|
|
20
|
+
private sync;
|
|
21
|
+
}
|
|
22
|
+
export {};
|