@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
|
@@ -1,123 +1,66 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
5
|
+
import { guardProductionPush } from '../../lib/guard-production-push.js';
|
|
6
|
+
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
|
+
// The parent theme must be installed before its child theme is written, and Global Styles
|
|
9
|
+
// belong to whichever theme is active, so they go last.
|
|
10
|
+
const PUSH_TARGETS = [
|
|
11
|
+
{ commandId: 'theme:version:push', label: 'theme versions' },
|
|
12
|
+
{ commandId: 'theme:template:push', label: 'templates and parts' },
|
|
13
|
+
{ commandId: 'theme:style:push', label: 'Global Styles' },
|
|
14
|
+
];
|
|
15
|
+
export default class Push extends LoopressCommand {
|
|
16
|
+
static description = 'Push everything theme related to WordPress: theme versions, templates and parts, then Global Styles';
|
|
13
17
|
static enableJsonFlag = true;
|
|
14
|
-
static examples = ['$ lps theme push', '$ lps theme push --
|
|
18
|
+
static examples = ['$ lps theme push', '$ lps theme push --env staging', '$ lps theme push --dry-run'];
|
|
15
19
|
static flags = {
|
|
16
|
-
...
|
|
17
|
-
...
|
|
18
|
-
force: Flags.boolean({ default: false, description: '
|
|
20
|
+
...LoopressCommand.dryRunFlag,
|
|
21
|
+
...LoopressCommand.yesFlag,
|
|
22
|
+
force: Flags.boolean({ default: false, description: 'Passed to `lps theme version push`' }),
|
|
19
23
|
};
|
|
20
24
|
async run() {
|
|
21
25
|
const { flags } = await this.parse(Push);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return { ...IN_SYNC };
|
|
54
|
-
}
|
|
55
|
-
if (isNoop(diff))
|
|
56
|
-
this.log('Refreshing themes pinned to "latest" to their newest releases.');
|
|
57
|
-
for (const a of diff.toInstall)
|
|
58
|
-
this.log(` + ${a.slug} ${a.version}`);
|
|
59
|
-
for (const p of diff.toPin)
|
|
60
|
-
this.log(` ~ ${p.slug} ${p.from} to ${p.to}`);
|
|
61
|
-
for (const s of diff.toRemove)
|
|
62
|
-
this.log(` - ${s}`);
|
|
63
|
-
for (const c of diff.collisions)
|
|
64
|
-
this.log(` ! ${c.slug} (take over)`);
|
|
65
|
-
if (this.dryRun)
|
|
66
|
-
return result(diff, 'dry-run', diff.toRemove, true);
|
|
67
|
-
if (!(await confirmUninstall(diff.toRemove, this.yes)))
|
|
68
|
-
this.error('Aborted.');
|
|
69
|
-
const response = await this.sync(manifest, force);
|
|
70
|
-
if (response.output.trim())
|
|
71
|
-
this.log(response.output.trim());
|
|
72
|
-
this.log('Themes synced.');
|
|
73
|
-
await this.recordSuccess();
|
|
74
|
-
return result(diff, 'success', response.removed ?? diff.toRemove, force);
|
|
75
|
-
}
|
|
76
|
-
async fetchInstanceLock() {
|
|
77
|
-
try {
|
|
78
|
-
const { composerLock } = await this.wp.get('loopress/v1/composer/lock');
|
|
79
|
-
return composerLock;
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
if (isNotFoundError(error))
|
|
83
|
-
return null;
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
guardForce(diff, force) {
|
|
88
|
-
if (force)
|
|
89
|
-
return;
|
|
90
|
-
if (diff.collisions.length > 0) {
|
|
91
|
-
const list = diff.collisions.map((c) => `${c.slug} (${c.installedVersion})`).join(', ');
|
|
92
|
-
this.error(`${list} installed outside Loopress. Re-run with --force to take them over, or remove them from loopress.json.`);
|
|
93
|
-
}
|
|
94
|
-
const downgrades = diff.toPin.filter((p) => isDowngrade(p.from, p.to));
|
|
95
|
-
if (downgrades.length > 0) {
|
|
96
|
-
const list = downgrades.map((p) => `${p.slug} ${p.from} to ${p.to}`).join(', ');
|
|
97
|
-
this.error(`Refusing to downgrade: ${list}. Re-run with --force.`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
async sync(manifest, force) {
|
|
101
|
-
try {
|
|
102
|
-
return await this.wp.post('loopress/v1/composer/sync', { force, intent: { themes: manifest }, lock: null }, { timeoutMs: SYNC_TIMEOUT_MS });
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (parseCollisions(error)) {
|
|
106
|
-
this.error('The site rejected the push: themes installed outside Loopress. Re-run with --force.');
|
|
26
|
+
// Guarded once here, like `lps push`. Steps get --production-confirmed rather than --yes,
|
|
27
|
+
// so the versions step still asks before uninstalling themes unless the user passed --yes.
|
|
28
|
+
await guardProductionPush({
|
|
29
|
+
dryRun: this.dryRun,
|
|
30
|
+
error: (message) => this.error(message),
|
|
31
|
+
siteConfig: this.siteConfig,
|
|
32
|
+
yes: this.yes,
|
|
33
|
+
});
|
|
34
|
+
// Nothing to install and no composer.json: the versions step would only fail on an empty
|
|
35
|
+
// manifest, which must not fail a templates or styles only project.
|
|
36
|
+
const hasVersions = Object.keys(this.localConfig.themes ?? {}).length > 0 || existsSync(join(process.cwd(), this.rootDir, 'composer.json'));
|
|
37
|
+
const targets = hasVersions ? PUSH_TARGETS : PUSH_TARGETS.filter((target) => target.commandId !== 'theme:version:push');
|
|
38
|
+
const results = [];
|
|
39
|
+
for (const target of targets) {
|
|
40
|
+
const argv = ['--env', this.siteConfig.name, '--production-confirmed'];
|
|
41
|
+
if (this.yes)
|
|
42
|
+
argv.push('--yes');
|
|
43
|
+
if (this.dryRun)
|
|
44
|
+
argv.push('--dry-run');
|
|
45
|
+
if (flags.force && target.commandId === 'theme:version:push')
|
|
46
|
+
argv.push('--force');
|
|
47
|
+
this.log(`\n→ Pushing ${target.label}...`);
|
|
48
|
+
try {
|
|
49
|
+
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand(target.commandId, argv));
|
|
50
|
+
this.log(`✓ ${target.label} pushed`);
|
|
51
|
+
results.push({ label: target.label, status: 'pushed' });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const { message } = error;
|
|
55
|
+
this.log(`✗ ${target.label} failed: ${message}`);
|
|
56
|
+
results.push({ error: message, label: target.label, status: 'failed' });
|
|
107
57
|
}
|
|
108
|
-
throw error;
|
|
109
58
|
}
|
|
59
|
+
const failures = results.filter((result) => result.status === 'failed');
|
|
60
|
+
if (failures.length > 0) {
|
|
61
|
+
const reasons = failures.map((result) => `${result.label}: ${result.error}`);
|
|
62
|
+
this.error(`${pluralize(failures.length, 'theme resource')} failed to push. ${reasons.join('; ')}`);
|
|
63
|
+
}
|
|
64
|
+
return { results };
|
|
110
65
|
}
|
|
111
66
|
}
|
|
112
|
-
function isNoop(diff) {
|
|
113
|
-
return (diff.toInstall.length === 0 && diff.toPin.length === 0 && diff.toRemove.length === 0 && diff.collisions.length === 0);
|
|
114
|
-
}
|
|
115
|
-
function result(diff, status, removed, includeCollisions) {
|
|
116
|
-
const collisionSlugs = includeCollisions ? diff.collisions.map((c) => c.slug) : [];
|
|
117
|
-
return {
|
|
118
|
-
installed: [...diff.toInstall.map((a) => a.slug), ...collisionSlugs],
|
|
119
|
-
pinned: diff.toPin.map((p) => p.slug),
|
|
120
|
-
removed,
|
|
121
|
-
status,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../../lib/base.js';
|
|
2
|
+
type ThemeStylePullResult = {
|
|
3
|
+
file: string;
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
2
6
|
export default class Pull extends LoopressCommand {
|
|
3
7
|
static args: {
|
|
4
8
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
9
|
};
|
|
6
10
|
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
7
12
|
static examples: string[];
|
|
8
13
|
static flags: {
|
|
9
14
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
15
|
};
|
|
11
|
-
run(): Promise<
|
|
16
|
+
run(): Promise<ThemeStylePullResult>;
|
|
12
17
|
}
|
|
18
|
+
export {};
|
|
@@ -8,6 +8,7 @@ export default class Pull extends LoopressCommand {
|
|
|
8
8
|
path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
|
|
9
9
|
};
|
|
10
10
|
static description = "Pull the active block theme's Global Styles customizations (Site Editor > Styles) from WordPress into a local file";
|
|
11
|
+
static enableJsonFlag = true;
|
|
11
12
|
static examples = ['$ lps theme style pull'];
|
|
12
13
|
static flags = {
|
|
13
14
|
...LoopressCommand.dryRunFlag,
|
|
@@ -22,10 +23,11 @@ export default class Pull extends LoopressCommand {
|
|
|
22
23
|
const file = join(dir, themeStylesFileName(stylesheet));
|
|
23
24
|
if (this.dryRun) {
|
|
24
25
|
this.log(`[dry-run] Would pull Global Styles for "${stylesheet}" to ${file}`);
|
|
25
|
-
return;
|
|
26
|
+
return { file, status: 'dry-run' };
|
|
26
27
|
}
|
|
27
28
|
await mkdir(dir, { recursive: true });
|
|
28
29
|
await writeFile(file, JSON.stringify(canonicalGlobalStyles(item), null, 2) + '\n');
|
|
29
30
|
this.log(`Pulled Global Styles for "${stylesheet}" to ${file}`);
|
|
31
|
+
return { file, status: 'success' };
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { PushCommand } from '../../../lib/push-command.js';
|
|
2
|
+
type ThemeStylePushResult = {
|
|
3
|
+
file: string;
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
2
6
|
export default class Push extends PushCommand {
|
|
3
7
|
static args: {
|
|
4
8
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
9
|
};
|
|
6
10
|
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
7
12
|
static examples: string[];
|
|
8
13
|
static flags: {
|
|
14
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
15
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
16
|
};
|
|
11
|
-
run(): Promise<
|
|
17
|
+
run(): Promise<ThemeStylePushResult>;
|
|
12
18
|
}
|
|
19
|
+
export {};
|
|
@@ -9,9 +9,11 @@ export default class Push extends PushCommand {
|
|
|
9
9
|
path: Args.string({ description: 'Path to theme styles directory (overrides project config)' }),
|
|
10
10
|
};
|
|
11
11
|
static description = "Push the local Global Styles file to the active block theme's Site Editor > Styles on WordPress";
|
|
12
|
+
static enableJsonFlag = true;
|
|
12
13
|
static examples = ['$ lps theme style push'];
|
|
13
14
|
static flags = {
|
|
14
15
|
...PushCommand.dryRunFlag,
|
|
16
|
+
...PushCommand.yesFlag,
|
|
15
17
|
};
|
|
16
18
|
async run() {
|
|
17
19
|
const { args } = await this.parse(Push);
|
|
@@ -32,7 +34,7 @@ export default class Push extends PushCommand {
|
|
|
32
34
|
}
|
|
33
35
|
if (this.dryRun) {
|
|
34
36
|
this.log(`[dry-run] Would push: ${file}`);
|
|
35
|
-
return;
|
|
37
|
+
return { file, status: 'dry-run' };
|
|
36
38
|
}
|
|
37
39
|
const provider = getResourceStateProvider('theme-styles');
|
|
38
40
|
const beforeState = await this.captureBeforePushState(provider, dir);
|
|
@@ -41,5 +43,6 @@ export default class Push extends PushCommand {
|
|
|
41
43
|
this.log(`Pushed: ${file}`);
|
|
42
44
|
await this.writeAfterPushSnapshot(provider, dir, beforeState);
|
|
43
45
|
await this.recordSuccess();
|
|
46
|
+
return { file, status: 'success' };
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DiffCommand } from '
|
|
2
|
-
import { getResourceStateProvider } from '
|
|
1
|
+
import { DiffCommand } from '../../../lib/diff-command.js';
|
|
2
|
+
import { getResourceStateProvider } from '../../../lib/resource-state.js';
|
|
3
3
|
export default class Diff extends DiffCommand {
|
|
4
4
|
static description = 'Show what differs in block templates and parts between your local files and a WordPress environment, or between two environments. Templates and parts edited in the Site Editor show up too.';
|
|
5
5
|
static enableJsonFlag = true;
|
|
6
|
-
static examples = ['$ lps template diff', '$ lps template diff --env staging', '$ lps template diff --env staging --against production'];
|
|
6
|
+
static examples = ['$ lps theme template diff', '$ lps theme template diff --env staging', '$ lps theme template diff --env staging --against production'];
|
|
7
7
|
static flags = { ...DiffCommand.againstFlag };
|
|
8
8
|
async run() {
|
|
9
9
|
const { flags } = await this.parse(Diff);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LoopressCommand } from '
|
|
2
|
-
import { type ChildTheme } from '
|
|
1
|
+
import { LoopressCommand } from '../../../lib/base.js';
|
|
2
|
+
import { type ChildTheme } from '../../../utils/template-format.js';
|
|
3
3
|
type ListResult = Omit<ChildTheme, 'parts' | 'templates'> & {
|
|
4
4
|
parts: string[];
|
|
5
5
|
templates: string[];
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { LoopressCommand } from '
|
|
2
|
-
import { pluralize } from '
|
|
3
|
-
import { CHILD_THEME_ENDPOINT } from '
|
|
1
|
+
import { LoopressCommand } from '../../../lib/base.js';
|
|
2
|
+
import { pluralize } from '../../../utils/pluralize.js';
|
|
3
|
+
import { CHILD_THEME_ENDPOINT } from '../../../utils/template-format.js';
|
|
4
4
|
export default class List extends LoopressCommand {
|
|
5
5
|
static description = 'Show the Loopress child theme of the active block theme: its templates, parts, and those edited in the Site Editor';
|
|
6
6
|
static enableJsonFlag = true;
|
|
7
|
-
static examples = ['$ lps template list'];
|
|
7
|
+
static examples = ['$ lps theme template list'];
|
|
8
8
|
async run() {
|
|
9
9
|
const child = await this.wp.get(CHILD_THEME_ENDPOINT);
|
|
10
10
|
// The markup is only needed by `template diff`, it would drown the JSON output here.
|
|
11
11
|
const result = { ...child, parts: child.parts.map((part) => part.slug), templates: child.templates.map((template) => template.slug) };
|
|
12
12
|
if (!child.exists) {
|
|
13
|
-
this.log(`No Loopress child theme yet (${child.stylesheet}), run lps template push.`);
|
|
13
|
+
this.log(`No Loopress child theme yet (${child.stylesheet}), run lps theme template push.`);
|
|
14
14
|
return result;
|
|
15
15
|
}
|
|
16
16
|
this.log(`${child.stylesheet} (child of ${child.parent}, ${child.active ? 'active' : 'not active'})`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { PushCommand } from '
|
|
1
|
+
import { PushCommand } from '../../../lib/push-command.js';
|
|
2
2
|
type PushResult = {
|
|
3
3
|
active?: boolean;
|
|
4
|
+
blockStyles: string[];
|
|
4
5
|
customized?: string[];
|
|
5
6
|
parts: string[];
|
|
6
7
|
status: 'dry-run' | 'success';
|
|
7
8
|
stylesheet?: string;
|
|
8
9
|
templates: string[];
|
|
10
|
+
unknownBlocks?: string[];
|
|
9
11
|
};
|
|
10
12
|
export default class Push extends PushCommand {
|
|
11
13
|
static description: string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PushCommand } from '../../../lib/push-command.js';
|
|
2
|
+
import { getResourceStateProvider } from '../../../lib/resource-state.js';
|
|
3
|
+
import { formatPageProblems } from '../../../utils/page-format.js';
|
|
4
|
+
import { pluralize } from '../../../utils/pluralize.js';
|
|
5
|
+
import { resolveResourceDir } from '../../../utils/resource-dirs.js';
|
|
6
|
+
import { CHILD_THEME_ENDPOINT, readLocalTemplates } from '../../../utils/template-format.js';
|
|
7
|
+
import { bundleThemeCss } from '../../../utils/theme-css.js';
|
|
8
|
+
export default class Push extends PushCommand {
|
|
9
|
+
static description = 'Push block templates (theme/templates/<slug>.html), template parts (theme/parts/<slug>.html) and the theme CSS (theme/css/style.css, theme/blocks/<namespace>/<block>.css, bundled with esbuild) as the files of a child theme of the active block theme, <parent>-loopress. The child mirrors the project: files removed locally are removed from it. It is never activated for you.';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['$ lps theme template push', '$ lps theme template push --dry-run'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...PushCommand.dryRunFlag,
|
|
14
|
+
...PushCommand.yesFlag,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
await this.parse(Push);
|
|
18
|
+
const templatesPath = resolveResourceDir('template', this.localConfig);
|
|
19
|
+
const partsPath = resolveResourceDir('part', this.localConfig);
|
|
20
|
+
this.log(`Pushing templates and parts to ${this.siteConfig.url}`);
|
|
21
|
+
this.log(`Templates path: ${templatesPath}, parts path: ${partsPath}`);
|
|
22
|
+
const { parts, problems, templates } = await readLocalTemplates(templatesPath, partsPath);
|
|
23
|
+
const { css, problems: cssProblems } = await bundleThemeCss(resolveResourceDir('css', this.localConfig), resolveResourceDir('blockCss', this.localConfig));
|
|
24
|
+
problems.push(...cssProblems);
|
|
25
|
+
if (problems.length > 0) {
|
|
26
|
+
this.error(`Nothing was pushed, fix these files first:\n ${formatPageProblems(problems)}`);
|
|
27
|
+
}
|
|
28
|
+
const names = {
|
|
29
|
+
blockStyles: css.blocks.map((block) => block.name),
|
|
30
|
+
parts: parts.map((part) => part.slug),
|
|
31
|
+
templates: templates.map((template) => template.slug),
|
|
32
|
+
};
|
|
33
|
+
this.log(`Found ${pluralize(templates.length, 'template')}, ${pluralize(parts.length, 'part')}, ` +
|
|
34
|
+
`${css.style ? 'a global stylesheet' : 'no global stylesheet'} and ${pluralize(css.blocks.length, 'block stylesheet')}`);
|
|
35
|
+
if (this.dryRun) {
|
|
36
|
+
await this.previewRemote(getResourceStateProvider('template'), templatesPath);
|
|
37
|
+
this.log('[dry-run] Would rewrite the child theme with them.');
|
|
38
|
+
return { ...names, status: 'dry-run' };
|
|
39
|
+
}
|
|
40
|
+
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts, templates });
|
|
41
|
+
await this.recordSuccess();
|
|
42
|
+
this.log(`Child theme ${child.stylesheet} written.`);
|
|
43
|
+
if (!child.active) {
|
|
44
|
+
this.warn(`${child.stylesheet} is not the active theme, so nothing changed on the site yet. Activate it in Appearance > Themes.`);
|
|
45
|
+
}
|
|
46
|
+
if (child.customized.length > 0) {
|
|
47
|
+
this.warn(`Edited in the Site Editor, their database copy still wins over the pushed file: ${child.customized.join(', ')}. ` +
|
|
48
|
+
'Clear their customizations in Appearance > Editor to use the pushed version.');
|
|
49
|
+
}
|
|
50
|
+
// A warning, never a refusal: the block may come from a plugin active on another environment.
|
|
51
|
+
const unknownBlocks = child.unknownBlocks ?? [];
|
|
52
|
+
if (unknownBlocks.length > 0) {
|
|
53
|
+
this.warn(`No block registered on this site under these names, their CSS will never load: ${unknownBlocks.join(', ')}. ` +
|
|
54
|
+
'Check the file name in theme/blocks/, or activate the plugin that provides the block.');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...names,
|
|
58
|
+
active: child.active,
|
|
59
|
+
customized: child.customized,
|
|
60
|
+
status: 'success',
|
|
61
|
+
stylesheet: child.stylesheet,
|
|
62
|
+
unknownBlocks,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../../lib/base.js';
|
|
2
|
+
import { type ComposerPullResult } from '../../../utils/composer.js';
|
|
3
|
+
type PullResult = {
|
|
4
|
+
added: string[];
|
|
5
|
+
merged: Record<string, string>;
|
|
6
|
+
skipped?: ComposerPullResult['skipped'];
|
|
7
|
+
status: 'dry-run' | 'success';
|
|
8
|
+
updated: Array<{
|
|
9
|
+
from: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
to: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export default class Pull extends LoopressCommand {
|
|
15
|
+
static description: string;
|
|
16
|
+
static enableJsonFlag: boolean;
|
|
17
|
+
static examples: string[];
|
|
18
|
+
static flags: {
|
|
19
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
};
|
|
21
|
+
run(): Promise<PullResult>;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { LoopressCommand } from '../../../lib/base.js';
|
|
4
|
+
import { pullIntoComposerJson } from '../../../utils/composer.js';
|
|
5
|
+
import { writeLocalConfig } from '../../../utils/loopress-config.js';
|
|
6
|
+
import { mergePluginManifest } from '../../../utils/plugins.js';
|
|
7
|
+
import { parseInstalledThemes } from '../../../utils/themes.js';
|
|
8
|
+
export default class Pull extends LoopressCommand {
|
|
9
|
+
static description = 'Pull installed themes from WordPress into loopress.json, pinned to their live versions';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['$ lps theme version pull'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...LoopressCommand.dryRunFlag,
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { url } = this.siteConfig;
|
|
17
|
+
this.log(`Pulling themes from ${url}`);
|
|
18
|
+
const raw = await this.wp.get('wp/v2/themes');
|
|
19
|
+
const installed = parseInstalledThemes(raw);
|
|
20
|
+
const incoming = Object.fromEntries(installed.map((t) => [t.slug, t.version]));
|
|
21
|
+
// A composer.json is authoritative over loopress.json: pin the live versions there instead.
|
|
22
|
+
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
23
|
+
if (existsSync(composerJsonPath)) {
|
|
24
|
+
const result = await pullIntoComposerJson(composerJsonPath, 'theme', incoming, {
|
|
25
|
+
dryRun: this.dryRun,
|
|
26
|
+
log: this.log.bind(this),
|
|
27
|
+
});
|
|
28
|
+
return { ...result, status: this.dryRun ? 'dry-run' : 'success' };
|
|
29
|
+
}
|
|
30
|
+
const { added, merged, updated } = mergePluginManifest(this.localConfig.themes ?? {}, incoming);
|
|
31
|
+
if (this.dryRun) {
|
|
32
|
+
this.log(`[dry-run] Would write ${Object.keys(merged).length} themes to loopress.json`);
|
|
33
|
+
return { added, merged, status: 'dry-run', updated };
|
|
34
|
+
}
|
|
35
|
+
await writeLocalConfig({ ...this.localConfig, themes: merged });
|
|
36
|
+
this.log(`Wrote ${Object.keys(merged).length} themes to loopress.json`);
|
|
37
|
+
if (added.length > 0)
|
|
38
|
+
this.log(` + Added: ${added.join(', ')}`);
|
|
39
|
+
for (const u of updated)
|
|
40
|
+
this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
41
|
+
return { added, merged, status: 'success', updated };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,123 @@
|
|
|
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';
|
|
5
|
+
import { PushCommand } from '../../../lib/push-command.js';
|
|
6
|
+
import { isNotFoundError } from '../../../lib/wp-client.js';
|
|
7
|
+
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS } from '../../../utils/plugin-sync.js';
|
|
8
|
+
import { lockedWpackagistSlugs } from '../../../utils/plugins.js';
|
|
9
|
+
import { diffThemes, parseInstalledThemes } from '../../../utils/themes.js';
|
|
10
|
+
const IN_SYNC = { installed: [], pinned: [], removed: [], status: 'in-sync' };
|
|
11
|
+
export default class Push extends PushCommand {
|
|
12
|
+
static description = 'Install themes on WordPress to match loopress.json (versions only, never switches the active theme)';
|
|
13
|
+
static enableJsonFlag = true;
|
|
14
|
+
static examples = ['$ lps theme version push', '$ lps theme version push --force'];
|
|
15
|
+
static flags = {
|
|
16
|
+
...PushCommand.dryRunFlag,
|
|
17
|
+
...PushCommand.yesFlag,
|
|
18
|
+
force: Flags.boolean({ default: false, description: 'Allow downgrades and take over themes installed outside Loopress' }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(Push);
|
|
22
|
+
const { force } = flags;
|
|
23
|
+
if (existsSync(join(process.cwd(), this.rootDir, 'composer.json'))) {
|
|
24
|
+
this.warn('This project has a composer.json, which is authoritative. Run `lps composer push` instead.');
|
|
25
|
+
return { ...IN_SYNC, status: 'composer-managed' };
|
|
26
|
+
}
|
|
27
|
+
const manifest = this.localConfig.themes ?? {};
|
|
28
|
+
if (Object.keys(manifest).length === 0) {
|
|
29
|
+
this.error('No themes found in loopress.json. Run `lps theme version pull` first.');
|
|
30
|
+
}
|
|
31
|
+
this.log(`Pushing themes to ${this.siteConfig.url}`);
|
|
32
|
+
const raw = await this.wp.get('wp/v2/themes');
|
|
33
|
+
const installed = parseInstalledThemes(raw);
|
|
34
|
+
const managed = lockedWpackagistSlugs(await this.fetchInstanceLock(), 'theme');
|
|
35
|
+
const diff = diffThemes(manifest, installed, managed);
|
|
36
|
+
this.guardForce(diff, force);
|
|
37
|
+
// Uninstalling the theme WordPress is currently rendering leaves the site with no active
|
|
38
|
+
// theme, which is a hard fatal. --force does not override this: switch the active theme
|
|
39
|
+
// on the site first, then the removal is safe. A --force takeover of a collision hits the
|
|
40
|
+
// same fatal: the server moves the existing folder out of the way before reinstalling it,
|
|
41
|
+
// so the active theme is briefly missing either way, an active collision is just as
|
|
42
|
+
// dangerous as an active removal.
|
|
43
|
+
const activeRemoval = [...diff.toRemove, ...diff.collisions.map((c) => c.slug)].find((slug) => installed.find((t) => t.slug === slug)?.active);
|
|
44
|
+
if (activeRemoval) {
|
|
45
|
+
this.error(`Refusing to uninstall "${activeRemoval}": it is the site's active theme. ` +
|
|
46
|
+
'Switch the site to another theme, then re-run to remove it.');
|
|
47
|
+
}
|
|
48
|
+
// A "latest" pin never shows as drift, but its newest upstream release may have moved, so
|
|
49
|
+
// a push must still run `composer update` to pick it up.
|
|
50
|
+
const hasLatestPin = Object.values(manifest).includes('latest');
|
|
51
|
+
if (!hasLatestPin && isNoop(diff)) {
|
|
52
|
+
this.log('Everything is already in sync.');
|
|
53
|
+
return { ...IN_SYNC };
|
|
54
|
+
}
|
|
55
|
+
if (isNoop(diff))
|
|
56
|
+
this.log('Refreshing themes pinned to "latest" to their newest releases.');
|
|
57
|
+
for (const a of diff.toInstall)
|
|
58
|
+
this.log(` + ${a.slug} ${a.version}`);
|
|
59
|
+
for (const p of diff.toPin)
|
|
60
|
+
this.log(` ~ ${p.slug} ${p.from} to ${p.to}`);
|
|
61
|
+
for (const s of diff.toRemove)
|
|
62
|
+
this.log(` - ${s}`);
|
|
63
|
+
for (const c of diff.collisions)
|
|
64
|
+
this.log(` ! ${c.slug} (take over)`);
|
|
65
|
+
if (this.dryRun)
|
|
66
|
+
return result(diff, 'dry-run', diff.toRemove, true);
|
|
67
|
+
if (!(await confirmUninstall(diff.toRemove, this.yes)))
|
|
68
|
+
this.error('Aborted.');
|
|
69
|
+
const response = await this.sync(manifest, force);
|
|
70
|
+
if (response.output.trim())
|
|
71
|
+
this.log(response.output.trim());
|
|
72
|
+
this.log('Themes synced.');
|
|
73
|
+
await this.recordSuccess();
|
|
74
|
+
return result(diff, 'success', response.removed ?? diff.toRemove, force);
|
|
75
|
+
}
|
|
76
|
+
async fetchInstanceLock() {
|
|
77
|
+
try {
|
|
78
|
+
const { composerLock } = await this.wp.get('loopress/v1/composer/lock');
|
|
79
|
+
return composerLock;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (isNotFoundError(error))
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
guardForce(diff, force) {
|
|
88
|
+
if (force)
|
|
89
|
+
return;
|
|
90
|
+
if (diff.collisions.length > 0) {
|
|
91
|
+
const list = diff.collisions.map((c) => `${c.slug} (${c.installedVersion})`).join(', ');
|
|
92
|
+
this.error(`${list} installed outside Loopress. Re-run with --force to take them over, or remove them from loopress.json.`);
|
|
93
|
+
}
|
|
94
|
+
const downgrades = diff.toPin.filter((p) => isDowngrade(p.from, p.to));
|
|
95
|
+
if (downgrades.length > 0) {
|
|
96
|
+
const list = downgrades.map((p) => `${p.slug} ${p.from} to ${p.to}`).join(', ');
|
|
97
|
+
this.error(`Refusing to downgrade: ${list}. Re-run with --force.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async sync(manifest, force) {
|
|
101
|
+
try {
|
|
102
|
+
return await this.wp.post('loopress/v1/composer/sync', { force, intent: { themes: manifest }, lock: null }, { timeoutMs: SYNC_TIMEOUT_MS });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (parseCollisions(error)) {
|
|
106
|
+
this.error('The site rejected the push: themes installed outside Loopress. Re-run with --force.');
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function isNoop(diff) {
|
|
113
|
+
return (diff.toInstall.length === 0 && diff.toPin.length === 0 && diff.toRemove.length === 0 && diff.collisions.length === 0);
|
|
114
|
+
}
|
|
115
|
+
function result(diff, status, removed, includeCollisions) {
|
|
116
|
+
const collisionSlugs = includeCollisions ? diff.collisions.map((c) => c.slug) : [];
|
|
117
|
+
return {
|
|
118
|
+
installed: [...diff.toInstall.map((a) => a.slug), ...collisionSlugs],
|
|
119
|
+
pinned: diff.toPin.map((p) => p.slug),
|
|
120
|
+
removed,
|
|
121
|
+
status,
|
|
122
|
+
};
|
|
123
|
+
}
|