@loopress/cli 0.32.2 → 0.33.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 +511 -154
- package/dist/commands/acf/list.d.ts +1 -1
- package/dist/commands/acf/list.js +3 -4
- package/dist/commands/acf/pull.d.ts +1 -1
- package/dist/commands/acf/pull.js +9 -6
- package/dist/commands/acf/push.d.ts +1 -1
- package/dist/commands/acf/push.js +5 -4
- package/dist/commands/api/publish.js +2 -1
- package/dist/commands/app/pull.js +2 -1
- package/dist/commands/app/push.js +5 -4
- package/dist/commands/app/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/app/{remove.js → rm.js} +2 -1
- package/dist/commands/block/diff.d.ts +2 -0
- package/dist/commands/block/diff.js +5 -0
- package/dist/commands/block/list.d.ts +8 -0
- package/dist/commands/block/list.js +23 -0
- package/dist/commands/block/pull.d.ts +21 -0
- package/dist/commands/block/pull.js +67 -0
- package/dist/commands/block/push.d.ts +20 -0
- package/dist/commands/block/push.js +48 -0
- package/dist/commands/block/rm.d.ts +23 -0
- package/dist/commands/block/rm.js +69 -0
- package/dist/commands/block/rollback.d.ts +2 -0
- package/dist/commands/block/rollback.js +5 -0
- package/dist/commands/composer/init.js +1 -0
- package/dist/commands/composer/push.js +5 -4
- package/dist/commands/content/copy.d.ts +3 -0
- package/dist/commands/content/copy.js +79 -2
- package/dist/commands/dev.js +4 -3
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +33 -3
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +37 -23
- package/dist/commands/form/pull.js +7 -5
- package/dist/commands/form/push.js +4 -3
- package/dist/commands/init.js +4 -3
- package/dist/commands/menu/push.js +8 -8
- package/dist/commands/option/list.js +5 -2
- package/dist/commands/option/push.js +2 -1
- package/dist/commands/option/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/option/{remove.js → rm.js} +2 -1
- package/dist/commands/page/push.js +2 -1
- package/dist/commands/plugin/audit.d.ts +4 -4
- package/dist/commands/plugin/audit.js +35 -15
- package/dist/commands/plugin/pull.js +3 -2
- package/dist/commands/plugin/push.js +2 -2
- package/dist/commands/plugin/status.js +3 -1
- package/dist/commands/project/config.js +7 -6
- package/dist/commands/project/pull.js +3 -1
- package/dist/commands/project/push.js +6 -4
- package/dist/commands/project/{remove.d.ts → rm.d.ts} +1 -0
- package/dist/commands/project/{remove.js → rm.js} +3 -2
- package/dist/commands/project/switch.js +6 -4
- package/dist/commands/promote.js +7 -2
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +42 -18
- package/dist/commands/push.d.ts +2 -1
- package/dist/commands/push.js +69 -25
- package/dist/commands/snippet/publish.js +9 -5
- package/dist/commands/snippet/push.d.ts +1 -1
- package/dist/commands/snippet/push.js +15 -12
- package/dist/commands/status.js +1 -0
- package/dist/commands/theme/activate.d.ts +21 -0
- package/dist/commands/theme/activate.js +44 -0
- package/dist/commands/theme/pull.js +2 -1
- package/dist/commands/theme/push.js +7 -3
- package/dist/commands/theme/style/push.js +7 -2
- package/dist/commands/theme/template/push.js +25 -6
- package/dist/config/auth.manager.d.ts +10 -7
- package/dist/config/auth.manager.js +5 -1
- package/dist/config/json-file.d.ts +1 -1
- package/dist/config/project-config.manager.d.ts +2 -2
- package/dist/config/project-config.manager.js +13 -21
- package/dist/lib/api-client.js +16 -21
- package/dist/lib/app-build.js +6 -2
- package/dist/lib/app-manifest.js +6 -6
- package/dist/lib/base.d.ts +2 -1
- package/dist/lib/base.js +10 -8
- package/dist/lib/bootstrap-full-install.js +8 -6
- package/dist/lib/capabilities.d.ts +44 -0
- package/dist/lib/capabilities.js +85 -0
- package/dist/lib/declared-type-command.js +4 -2
- package/dist/lib/dev-watch.js +5 -5
- package/dist/lib/diff-command.d.ts +2 -3
- package/dist/lib/diff-command.js +9 -7
- package/dist/lib/diff-state.js +2 -3
- package/dist/lib/load-files.js +2 -1
- package/dist/lib/load-snippets.js +37 -42
- package/dist/lib/local-callback-server.js +7 -1
- package/dist/lib/narrow.d.ts +5 -0
- package/dist/lib/narrow.js +26 -0
- package/dist/lib/php-files-command.js +8 -5
- package/dist/lib/plugin-detection.d.ts +0 -1
- package/dist/lib/plugin-detection.js +2 -4
- package/dist/lib/push-command.d.ts +1 -1
- package/dist/lib/push-command.js +10 -5
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.js +38 -13
- package/dist/lib/rollback-command.d.ts +1 -1
- package/dist/lib/rollback-command.js +3 -2
- package/dist/lib/rollback-materialize.js +28 -8
- package/dist/lib/rotate-app-password.js +1 -0
- package/dist/lib/sentry.js +2 -2
- package/dist/lib/snapshot-store.d.ts +10 -7
- package/dist/lib/snapshot-store.js +21 -5
- package/dist/lib/spinner.js +4 -0
- package/dist/lib/validate-local.js +35 -20
- package/dist/lib/wp-client.d.ts +6 -1
- package/dist/lib/wp-client.js +54 -69
- package/dist/lib/wp-site-diagnostic.js +3 -5
- package/dist/types/global-config.generated.d.ts +1 -1
- package/dist/types/plugin.d.ts +7 -5
- package/dist/types/plugin.js +6 -1
- package/dist/types/project-config.generated.d.ts +9 -1
- package/dist/types/snippet.d.ts +2 -2
- package/dist/types/snippet.js +1 -1
- package/dist/utils/acf-format.js +6 -3
- package/dist/utils/block-format.d.ts +27 -0
- package/dist/utils/block-format.js +93 -0
- package/dist/utils/composer.d.ts +7 -15
- package/dist/utils/composer.js +43 -16
- package/dist/utils/declared-type-format.js +26 -19
- package/dist/utils/form-format.js +2 -2
- package/dist/utils/loopress-config.js +9 -1
- package/dist/utils/menu-format.d.ts +3 -1
- package/dist/utils/menu-format.js +5 -0
- package/dist/utils/option-format.d.ts +2 -2
- package/dist/utils/option-format.js +15 -25
- package/dist/utils/page-format.js +7 -6
- package/dist/utils/php-top-level.js +15 -4
- package/dist/utils/plugin-sync.d.ts +7 -7
- package/dist/utils/plugin-sync.js +11 -11
- package/dist/utils/plugins.js +9 -6
- package/dist/utils/resource-dirs.d.ts +2 -0
- package/dist/utils/resource-dirs.js +9 -1
- package/dist/utils/snippet-format.d.ts +1 -1
- package/dist/utils/snippet-format.js +5 -3
- package/dist/utils/template-format.js +2 -2
- package/dist/utils/theme-css.js +14 -6
- package/dist/utils/theme-styles-format.d.ts +3 -0
- package/dist/utils/theme-styles-format.js +15 -5
- package/dist/utils/version.js +1 -1
- package/oclif.manifest.json +1938 -1393
- package/package.json +42 -17
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +11 -1
package/dist/commands/push.js
CHANGED
|
@@ -2,17 +2,22 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { LoopressCommand } from '../lib/base.js';
|
|
5
|
+
import { integrationFix, missingIntegration, missingModule } from '../lib/capabilities.js';
|
|
5
6
|
import { guardProductionPush } from '../lib/guard-production-push.js';
|
|
6
7
|
import { stdoutToStderr } from '../lib/json-delegation.js';
|
|
8
|
+
import { errorMessage } from '../lib/narrow.js';
|
|
7
9
|
import { PushCommand } from '../lib/push-command.js';
|
|
8
10
|
import { isLocalEnvironment } from '../lib/wp-client.js';
|
|
9
|
-
import { wpackagistRequire } from '../utils/composer.js';
|
|
10
|
-
import { projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
|
|
11
|
+
import { parseComposerJson, wpackagistRequire } from '../utils/composer.js';
|
|
12
|
+
import { projectHasChildThemeFiles, projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
|
|
11
13
|
import { globalStylesSkipNote } from '../utils/theme-styles-format.js';
|
|
12
14
|
// Backed by a plugin the site may not have (ACF, a form or a snippet plugin): a project without
|
|
13
15
|
// the resource's directory doesn't use it, and its push, dry run included, would only stop on the
|
|
14
16
|
// site's 409 or the missing directory.
|
|
15
17
|
const PLUGIN_BACKED = { 'acf:push': 'acf', 'form:push': 'form', 'snippet:push': 'snippets' };
|
|
18
|
+
// Features of Loopress Full only: on Light, skipped while the project doesn't use them, refused
|
|
19
|
+
// upfront when it does. On Full they run as usual, used or not.
|
|
20
|
+
const FULL_ONLY = { 'api:push': 'api', 'block:push': 'block', 'hook:push': 'hooks', 'page:push': 'page', 'pattern:push': 'pattern' };
|
|
16
21
|
// Dependency-ish order: code first (plugins, composer), then the ACF definitions content
|
|
17
22
|
// relies on, then content itself.
|
|
18
23
|
const PUSH_TARGETS = [
|
|
@@ -26,6 +31,8 @@ const PUSH_TARGETS = [
|
|
|
26
31
|
{ commandId: 'acf:push', label: 'ACF' },
|
|
27
32
|
{ commandId: 'api:push', label: 'API routes' },
|
|
28
33
|
{ commandId: 'hook:push', label: 'hooks' },
|
|
34
|
+
// Before templates and pages, which place them (`wp:loopress/<slug>`).
|
|
35
|
+
{ commandId: 'block:push', label: 'dynamic blocks' },
|
|
29
36
|
{ commandId: 'form:push', label: 'forms' },
|
|
30
37
|
// Before templates and pages, which can reference them (`wp:pattern`).
|
|
31
38
|
{ commandId: 'pattern:push', label: 'patterns' },
|
|
@@ -40,7 +47,7 @@ const PUSH_TARGETS = [
|
|
|
40
47
|
{ commandId: 'snippet:push', label: 'snippets' },
|
|
41
48
|
];
|
|
42
49
|
export default class Push extends LoopressCommand {
|
|
43
|
-
static description = 'Push every local resource to WordPress in one run: plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, forms, patterns, templates and parts, Global Styles, pages, menus, options, and snippets';
|
|
50
|
+
static description = 'Push every local resource to WordPress in one run: plugins, composer dependencies, post types, taxonomies, post meta, roles, ACF, API routes, hooks, dynamic blocks, forms, patterns, templates and parts, Global Styles, pages, menus, options, and snippets';
|
|
44
51
|
static enableJsonFlag = true;
|
|
45
52
|
static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run'];
|
|
46
53
|
static flags = {
|
|
@@ -50,7 +57,6 @@ export default class Push extends LoopressCommand {
|
|
|
50
57
|
};
|
|
51
58
|
async run() {
|
|
52
59
|
await this.guardProductionPush();
|
|
53
|
-
this.warnRemovedSeoModule();
|
|
54
60
|
const argv = this.buildArgv();
|
|
55
61
|
const results = [];
|
|
56
62
|
const skips = await this.skippedTargets();
|
|
@@ -68,7 +74,7 @@ export default class Push extends LoopressCommand {
|
|
|
68
74
|
results.push({ label: target.label, result, status: 'pushed' });
|
|
69
75
|
}
|
|
70
76
|
catch (error) {
|
|
71
|
-
const
|
|
77
|
+
const message = errorMessage(error);
|
|
72
78
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
73
79
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
74
80
|
// Stop here: later targets depend on earlier ones (hooks use what composer installs,
|
|
@@ -81,6 +87,28 @@ export default class Push extends LoopressCommand {
|
|
|
81
87
|
this.log(this.dryRun ? '\nEvery resource would push.' : '\nAll resources pushed.');
|
|
82
88
|
return { results };
|
|
83
89
|
}
|
|
90
|
+
// A step the project uses on a site that can't hold it would only fail at its turn, after
|
|
91
|
+
// everything before it was pushed: refuse the whole run upfront, saying how to fix it. An unused
|
|
92
|
+
// one is skipped, saying why when the site lacks it.
|
|
93
|
+
applySiteSteps(steps, skips, declared) {
|
|
94
|
+
const blockers = [];
|
|
95
|
+
for (const { commandId, missing, skipUnused, used } of steps) {
|
|
96
|
+
if (used && missing) {
|
|
97
|
+
const label = PUSH_TARGETS.find((target) => target.commandId === commandId)?.label;
|
|
98
|
+
const fix = integrationFix(missing, declared);
|
|
99
|
+
const fixSentence = fix ? ` ${fix}.` : '';
|
|
100
|
+
blockers.push(`${label}: ${missing.reason} on ${this.siteConfig.name}.${fixSentence}`);
|
|
101
|
+
}
|
|
102
|
+
else if (!used && missing?.needsFull) {
|
|
103
|
+
skips.set(commandId, { note: `${missing.reason}, skipped`, status: 'skipped' });
|
|
104
|
+
}
|
|
105
|
+
else if (!used && skipUnused) {
|
|
106
|
+
skips.set(commandId, { note: 'not configured, skipped', status: 'skipped' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (blockers.length > 0)
|
|
110
|
+
this.error(`Nothing was pushed, the project uses what this site can't hold:\n ${blockers.join('\n ')}`);
|
|
111
|
+
}
|
|
84
112
|
// Every delegated push always gets --yes: either this command's own guard already confirmed
|
|
85
113
|
// production once above, or the target isn't production and there's nothing to confirm.
|
|
86
114
|
// Without this, each delegated command would re-run its own production guard.
|
|
@@ -93,24 +121,22 @@ export default class Push extends LoopressCommand {
|
|
|
93
121
|
argv.push('--overwrite');
|
|
94
122
|
return argv;
|
|
95
123
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
composerJsonPath() {
|
|
125
|
+
return join(process.cwd(), this.rootDir, 'composer.json');
|
|
126
|
+
}
|
|
127
|
+
// The wordpress.org plugins the project declares: composer.json's wpackagist plugins when it
|
|
128
|
+
// exists, loopress.json's otherwise. Unreadable or invalid composer.json: none, the commands report it.
|
|
129
|
+
async declaredPlugins(hasComposerJson) {
|
|
130
|
+
if (!hasComposerJson)
|
|
131
|
+
return Object.keys(this.localConfig.plugins ?? {});
|
|
102
132
|
try {
|
|
103
|
-
const composerJson =
|
|
104
|
-
return Object.keys(wpackagistRequire(composerJson, 'plugin', isLocalEnvironment(this.siteConfig.name)))
|
|
133
|
+
const composerJson = parseComposerJson(await readFile(this.composerJsonPath(), 'utf8'));
|
|
134
|
+
return Object.keys(wpackagistRequire(composerJson, 'plugin', isLocalEnvironment(this.siteConfig.name)));
|
|
105
135
|
}
|
|
106
136
|
catch {
|
|
107
|
-
|
|
108
|
-
return false;
|
|
137
|
+
return [];
|
|
109
138
|
}
|
|
110
139
|
}
|
|
111
|
-
composerJsonPath() {
|
|
112
|
-
return join(process.cwd(), this.rootDir, 'composer.json');
|
|
113
|
-
}
|
|
114
140
|
// The site unreachable or the theme list refused: let the step run and report it itself.
|
|
115
141
|
async globalStylesSkipNote() {
|
|
116
142
|
try {
|
|
@@ -136,19 +162,37 @@ export default class Push extends LoopressCommand {
|
|
|
136
162
|
const skips = new Map();
|
|
137
163
|
const notConfigured = { note: 'not configured, skipped', status: 'skipped' };
|
|
138
164
|
const hasComposerJson = existsSync(this.composerJsonPath());
|
|
139
|
-
const
|
|
165
|
+
const declaredPlugins = await this.declaredPlugins(hasComposerJson);
|
|
166
|
+
// A composer.json that declares plugins is pushed whole by `plugin:push` (with its activation
|
|
167
|
+
// safety), so `composer:push` would only run Composer on the server a second time.
|
|
168
|
+
const composerPushedByPlugins = hasComposerJson && declaredPlugins.length > 0;
|
|
140
169
|
if (composerPushedByPlugins)
|
|
141
170
|
skips.set('composer:push', { note: 'already pushed whole by plugins', status: 'pushed' });
|
|
142
171
|
else if (!hasComposerJson)
|
|
143
172
|
skips.set('composer:push', notConfigured);
|
|
144
173
|
// With a composer.json, plugins come from it only; otherwise from loopress.json.
|
|
145
|
-
|
|
146
|
-
if (!pluginsConfigured)
|
|
174
|
+
if (declaredPlugins.length === 0)
|
|
147
175
|
skips.set('plugin:push', notConfigured);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
const capabilities = await this.capabilities();
|
|
177
|
+
const missingDependencies = missingModule('dependencies', 'plugins and Composer', capabilities);
|
|
178
|
+
const steps = [
|
|
179
|
+
...['plugin:push', 'composer:push'].map((commandId) => ({ commandId, missing: missingDependencies, skipUnused: false, used: !skips.has(commandId) })),
|
|
180
|
+
...Object.entries(PLUGIN_BACKED).map(([commandId, kind]) => ({
|
|
181
|
+
commandId,
|
|
182
|
+
missing: missingIntegration(kind, capabilities),
|
|
183
|
+
skipUnused: true,
|
|
184
|
+
used: projectUses(kind, this.localConfig),
|
|
185
|
+
})),
|
|
186
|
+
...Object.entries(FULL_ONLY).map(([commandId, kind]) => ({
|
|
187
|
+
commandId,
|
|
188
|
+
missing: missingIntegration(kind, capabilities),
|
|
189
|
+
skipUnused: false,
|
|
190
|
+
used: projectUses(kind, this.localConfig),
|
|
191
|
+
})),
|
|
192
|
+
// Without templates, parts or CSS, the step would only write an empty child theme.
|
|
193
|
+
{ commandId: 'theme:template:push', missing: missingIntegration('template', capabilities), skipUnused: true, used: projectHasChildThemeFiles(this.localConfig) },
|
|
194
|
+
];
|
|
195
|
+
this.applySiteSteps(steps, skips, { fromComposer: hasComposerJson, plugins: declaredPlugins });
|
|
152
196
|
const stylesNote = await this.globalStylesSkipNote();
|
|
153
197
|
if (stylesNote)
|
|
154
198
|
skips.set('theme:style:push', { note: stylesNote, status: 'skipped' });
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Args, Command } from '@oclif/core';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { ApiClient } from '../../lib/api-client.js';
|
|
4
3
|
import { loadSnippets } from '../../lib/load-snippets.js';
|
|
4
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
5
5
|
import { resolveLinkedProject } from '../../lib/resolve-linked-project.js';
|
|
6
|
+
import {} from '../../types/snippet.js';
|
|
6
7
|
import { pluralize } from '../../utils/pluralize.js';
|
|
8
|
+
import { resolveResourceDir } from '../../utils/resource-dirs.js';
|
|
7
9
|
import { toSlug } from '../../utils/to-slug.js';
|
|
8
10
|
// Publishes to the Loopress api (not a WordPress site), so this does not extend
|
|
9
11
|
// `LoopressCommand`/`PushCommand`: those force an environment to be resolved, but a project
|
|
@@ -18,14 +20,16 @@ export default class Publish extends Command {
|
|
|
18
20
|
async run() {
|
|
19
21
|
const { args } = await this.parse(Publish);
|
|
20
22
|
const { apiProjectId, localConfig, token } = await resolveLinkedProject((message) => this.error(message));
|
|
21
|
-
const path =
|
|
23
|
+
const path = resolveResourceDir('snippets', localConfig, args.path);
|
|
22
24
|
this.log(`Publishing snippets from ${path}`);
|
|
23
25
|
let snippets;
|
|
24
26
|
try {
|
|
25
|
-
snippets = await loadSnippets(path)
|
|
27
|
+
snippets = await loadSnippets(path, (message) => {
|
|
28
|
+
this.warn(message);
|
|
29
|
+
});
|
|
26
30
|
}
|
|
27
31
|
catch (error) {
|
|
28
|
-
this.error(error
|
|
32
|
+
this.error(errorMessage(error));
|
|
29
33
|
}
|
|
30
34
|
const api = new ApiClient(token);
|
|
31
35
|
try {
|
|
@@ -34,7 +38,7 @@ export default class Publish extends Command {
|
|
|
34
38
|
});
|
|
35
39
|
}
|
|
36
40
|
catch (error) {
|
|
37
|
-
this.error(error
|
|
41
|
+
this.error(errorMessage(error));
|
|
38
42
|
}
|
|
39
43
|
this.log(`Published ${pluralize(snippets.length, 'snippet')} to your Loopress account.`);
|
|
40
44
|
}
|
|
@@ -2,10 +2,13 @@ import { Args } from '@oclif/core';
|
|
|
2
2
|
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, extname, join } from 'node:path';
|
|
4
4
|
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
5
|
+
import { errorCode, errorMessage, isRecord } from '../../lib/narrow.js';
|
|
5
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
7
|
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
7
8
|
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
8
9
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
10
|
+
import {} from '../../types/snippet.generated.js';
|
|
11
|
+
import {} from '../../types/snippet.js';
|
|
9
12
|
import { pluralize } from '../../utils/pluralize.js';
|
|
10
13
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
11
14
|
import { toSlug } from '../../utils/to-slug.js';
|
|
@@ -65,18 +68,18 @@ export default class Push extends PushCommand {
|
|
|
65
68
|
const currentBase = basename(snippet.path, ext);
|
|
66
69
|
const canonicalBase = `${id}-${toSlug(name)}`;
|
|
67
70
|
const oldMetaPath = join(dir, `${currentBase}.json`);
|
|
68
|
-
let
|
|
71
|
+
let existing = {};
|
|
69
72
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const parsed = JSON.parse(await readFile(oldMetaPath, 'utf8'));
|
|
74
|
+
if (isRecord(parsed))
|
|
75
|
+
existing = parsed;
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
74
|
-
if (error
|
|
78
|
+
if (errorCode(error) !== 'ENOENT')
|
|
75
79
|
throw error;
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
meta
|
|
79
|
-
meta.type = snippet.type;
|
|
81
|
+
// Spread, not rebuilt: keys the CLI doesn't know about survive, in their original order.
|
|
82
|
+
const meta = { ...existing, id, name, type: snippet.type };
|
|
80
83
|
// Persist the id against the *current* file pairing before renaming anything, so a
|
|
81
84
|
// crash between the rename and the sidecar write still leaves a valid `<name>.<ext>` /
|
|
82
85
|
// `<name>.json` pair with the id on disk, and a retried `snippet push` won't re-create
|
|
@@ -95,7 +98,7 @@ export default class Push extends PushCommand {
|
|
|
95
98
|
return await loadSnippetsFromDisk(path, (message) => { this.warn(message); });
|
|
96
99
|
}
|
|
97
100
|
catch (error) {
|
|
98
|
-
this.error(error
|
|
101
|
+
this.error(errorMessage(error));
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
async pushSnippet(snippet, task) {
|
|
@@ -134,15 +137,15 @@ export default class Push extends PushCommand {
|
|
|
134
137
|
id = createdSnippet.id;
|
|
135
138
|
await this.ensureCanonicalFilename(snippet, createdSnippet.id, createdSnippet.name);
|
|
136
139
|
}
|
|
137
|
-
else {
|
|
138
|
-
await this.ensureCanonicalFilename(snippet,
|
|
140
|
+
else if (id !== undefined) {
|
|
141
|
+
await this.ensureCanonicalFilename(snippet, id, snippet.name);
|
|
139
142
|
}
|
|
140
143
|
if (task)
|
|
141
144
|
task.output = `Pushed: ${snippet.name}`;
|
|
142
145
|
return id;
|
|
143
146
|
}
|
|
144
147
|
catch (error) {
|
|
145
|
-
this.reportTaskFailure(`Failed to push ${snippet.name}: ${error
|
|
148
|
+
this.reportTaskFailure(`Failed to push ${snippet.name}: ${errorMessage(error)}`, error, task);
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
toPayload(snippet) {
|
|
@@ -172,7 +175,7 @@ export default class Push extends PushCommand {
|
|
|
172
175
|
pathsById.set(id, [...(pathsById.get(id) ?? []), path]);
|
|
173
176
|
}
|
|
174
177
|
return snippets.filter((snippet) => {
|
|
175
|
-
const paths = snippet.id === undefined ? [] : pathsById.get(snippet.id);
|
|
178
|
+
const paths = snippet.id === undefined ? [] : (pathsById.get(snippet.id) ?? []);
|
|
176
179
|
if (paths.length < 2)
|
|
177
180
|
return true;
|
|
178
181
|
this.failedCount++;
|
package/dist/commands/status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags, ux } from '@oclif/core';
|
|
2
2
|
import { configManager } from '../config/project-config.manager.js';
|
|
3
|
+
import {} from '../types/config.js';
|
|
3
4
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
4
5
|
const c = ux.colorize;
|
|
5
6
|
export default class Status extends Command {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type ActivateResult = {
|
|
3
|
+
active: string;
|
|
4
|
+
previous: string;
|
|
5
|
+
status: 'activated' | 'dry-run' | 'unchanged';
|
|
6
|
+
};
|
|
7
|
+
export default class Activate extends LoopressCommand {
|
|
8
|
+
static args: {
|
|
9
|
+
stylesheet: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static description: string;
|
|
12
|
+
static enableJsonFlag: boolean;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
static flags: {
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<ActivateResult>;
|
|
19
|
+
private childStylesheet;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
3
|
+
import { activateTheme } from '../../lib/capabilities.js';
|
|
4
|
+
import { guardProductionPush } from '../../lib/guard-production-push.js';
|
|
5
|
+
export default class Activate extends LoopressCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
stylesheet: Args.string({ description: 'Theme to activate (default: the Loopress child theme, <parent>-loopress)' }),
|
|
8
|
+
};
|
|
9
|
+
static description = 'Activate a theme on WordPress, the Loopress child theme by default. A child theme without settings of its own starts from a copy of its parent\'s theme mods and Global Styles. Set "activateChildTheme": true in loopress.json to have `lps theme template push` keep the child active.';
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
static examples = ['$ lps theme activate', '$ lps theme activate twentytwentyfive --env staging', '$ lps theme activate --dry-run'];
|
|
12
|
+
static flags = {
|
|
13
|
+
...LoopressCommand.dryRunFlag,
|
|
14
|
+
...LoopressCommand.yesFlag,
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { args } = await this.parse(Activate);
|
|
18
|
+
const { theme } = await this.capabilities();
|
|
19
|
+
const stylesheet = args.stylesheet ?? this.childStylesheet(theme);
|
|
20
|
+
if (stylesheet === theme.active) {
|
|
21
|
+
this.log(`${stylesheet} is already the active theme.`);
|
|
22
|
+
return { active: stylesheet, previous: theme.active, status: 'unchanged' };
|
|
23
|
+
}
|
|
24
|
+
if (this.dryRun) {
|
|
25
|
+
this.log(`[dry-run] Would activate ${stylesheet} instead of ${theme.active}.`);
|
|
26
|
+
return { active: stylesheet, previous: theme.active, status: 'dry-run' };
|
|
27
|
+
}
|
|
28
|
+
await guardProductionPush({ dryRun: this.dryRun, error: (message) => this.error(message), siteConfig: this.siteConfig, yes: this.yes });
|
|
29
|
+
const activation = await activateTheme(this.wp, stylesheet);
|
|
30
|
+
this.log(`Activated ${activation.active} (was ${activation.previous}).`);
|
|
31
|
+
return { active: activation.active, previous: activation.previous, status: 'activated' };
|
|
32
|
+
}
|
|
33
|
+
childStylesheet(theme) {
|
|
34
|
+
if (theme.child === undefined) {
|
|
35
|
+
this.error('The Loopress child theme needs Loopress Full. Name the theme to activate instead.');
|
|
36
|
+
}
|
|
37
|
+
else if (theme.child === null) {
|
|
38
|
+
this.error(`The active theme "${theme.active}" can't have a Loopress child theme: it needs a block theme that isn't already a child theme.`);
|
|
39
|
+
}
|
|
40
|
+
if (!theme.child.exists)
|
|
41
|
+
this.error(`${theme.child.stylesheet} doesn't exist yet. Run \`lps theme template push\` first.`);
|
|
42
|
+
return theme.child.stylesheet;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
2
|
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
3
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
3
4
|
import { pluralize } from '../../utils/pluralize.js';
|
|
4
5
|
// Templates and parts have no pull: the child theme is written from the project, never read back.
|
|
5
6
|
const PULL_TARGETS = [
|
|
@@ -26,7 +27,7 @@ export default class Pull extends LoopressCommand {
|
|
|
26
27
|
results.push({ label: target.label, status: 'pulled' });
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
|
-
const
|
|
30
|
+
const message = errorMessage(error);
|
|
30
31
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
31
32
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
32
33
|
}
|
|
@@ -4,7 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { LoopressCommand } from '../../lib/base.js';
|
|
5
5
|
import { guardProductionPush } from '../../lib/guard-production-push.js';
|
|
6
6
|
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
7
|
+
import { errorMessage } from '../../lib/narrow.js';
|
|
7
8
|
import { pluralize } from '../../utils/pluralize.js';
|
|
9
|
+
import { projectHasChildThemeFiles } from '../../utils/resource-dirs.js';
|
|
8
10
|
// The parent theme must be installed before its child theme is written, and Global Styles
|
|
9
11
|
// belong to whichever theme is active, so they go last.
|
|
10
12
|
const PUSH_TARGETS = [
|
|
@@ -13,7 +15,7 @@ const PUSH_TARGETS = [
|
|
|
13
15
|
{ commandId: 'theme:style:push', label: 'Global Styles' },
|
|
14
16
|
];
|
|
15
17
|
export default class Push extends LoopressCommand {
|
|
16
|
-
static description = 'Push everything theme related to WordPress: theme versions from loopress.json, then block templates and parts, then Global Styles.
|
|
18
|
+
static description = 'Push everything theme related to WordPress: theme versions from loopress.json, then block templates and parts, then Global Styles. Switches the active theme only to the Loopress child, when loopress.json sets "activateChildTheme": true.';
|
|
17
19
|
static enableJsonFlag = true;
|
|
18
20
|
static examples = ['$ lps theme push', '$ lps theme push --env staging', '$ lps theme push --dry-run'];
|
|
19
21
|
static flags = {
|
|
@@ -34,7 +36,9 @@ export default class Push extends LoopressCommand {
|
|
|
34
36
|
// Nothing to install and no composer.json: the versions step would only fail on an empty
|
|
35
37
|
// manifest, which must not fail a templates or styles only project.
|
|
36
38
|
const hasVersions = Object.keys(this.localConfig.themes ?? {}).length > 0 || existsSync(join(process.cwd(), this.rootDir, 'composer.json'));
|
|
37
|
-
|
|
39
|
+
// Same for templates: without templates, parts or CSS the step would refuse (nothing to push).
|
|
40
|
+
const hasChildTheme = projectHasChildThemeFiles(this.localConfig);
|
|
41
|
+
const targets = PUSH_TARGETS.filter((target) => (hasVersions || target.commandId !== 'theme:version:push') && (hasChildTheme || target.commandId !== 'theme:template:push'));
|
|
38
42
|
const results = [];
|
|
39
43
|
for (const target of targets) {
|
|
40
44
|
const argv = ['--env', this.siteConfig.name, '--production-confirmed'];
|
|
@@ -51,7 +55,7 @@ export default class Push extends LoopressCommand {
|
|
|
51
55
|
results.push({ label: target.label, result, status: 'pushed' });
|
|
52
56
|
}
|
|
53
57
|
catch (error) {
|
|
54
|
-
const
|
|
58
|
+
const message = errorMessage(error);
|
|
55
59
|
this.log(`✗ ${target.label} failed: ${message}`);
|
|
56
60
|
results.push({ error: message, label: target.label, status: 'failed' });
|
|
57
61
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { errorCode, isRecord } from '../../../lib/narrow.js';
|
|
4
5
|
import { PushCommand } from '../../../lib/push-command.js';
|
|
5
6
|
import { getResourceStateProvider } from '../../../lib/resource-state.js';
|
|
6
7
|
import { getActiveThemeGlobalStyles, globalStylesEndpoint, themeStylesFileName } from '../../../utils/theme-styles-format.js';
|
|
@@ -27,7 +28,7 @@ export default class Push extends PushCommand {
|
|
|
27
28
|
raw = await readFile(file, 'utf8');
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
|
-
if (error
|
|
31
|
+
if (errorCode(error) === 'ENOENT') {
|
|
31
32
|
this.error(`No local Global Styles file found for the active theme ("${file}"). Run \`lps theme style pull\` first.`);
|
|
32
33
|
}
|
|
33
34
|
throw error;
|
|
@@ -38,7 +39,11 @@ export default class Push extends PushCommand {
|
|
|
38
39
|
if (this.dryRun)
|
|
39
40
|
return { changes: this.previewChanges, file, status: 'dry-run' };
|
|
40
41
|
const local = JSON.parse(raw);
|
|
41
|
-
|
|
42
|
+
// Never fall back to empty settings/styles: that push would wipe the site's customizations.
|
|
43
|
+
if (!isRecord(local))
|
|
44
|
+
throw new Error(`${file} is not a JSON object`);
|
|
45
|
+
const { settings, styles } = local;
|
|
46
|
+
await this.wp.post(globalStylesEndpoint(id), { settings: settings ?? {}, styles: styles ?? {} });
|
|
42
47
|
this.log(`Pushed: ${file}`);
|
|
43
48
|
await this.writeAfterPushSnapshot(provider, dir, beforeState);
|
|
44
49
|
await this.recordSuccess();
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { activateTheme } from '../../../lib/capabilities.js';
|
|
3
|
+
import {} from '../../../lib/diff-state.js';
|
|
4
|
+
import { errorMessage } from '../../../lib/narrow.js';
|
|
2
5
|
import { logChanges, pushChanges, PushCommand } from '../../../lib/push-command.js';
|
|
3
6
|
import { formatPageProblems } from '../../../utils/page-format.js';
|
|
4
7
|
import { pluralize } from '../../../utils/pluralize.js';
|
|
5
|
-
import { resolveResourceDir } from '../../../utils/resource-dirs.js';
|
|
8
|
+
import { projectHasChildThemeFiles, resolveResourceDir } from '../../../utils/resource-dirs.js';
|
|
6
9
|
import { CHILD_THEME_ENDPOINT, readLocalTemplates } from '../../../utils/template-format.js';
|
|
7
10
|
import { bundleThemeCss } from '../../../utils/theme-css.js';
|
|
8
11
|
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
|
|
12
|
+
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 activated only when loopress.json sets "activateChildTheme": true, see `lps theme activate`.';
|
|
10
13
|
static enableJsonFlag = true;
|
|
11
14
|
static examples = ['$ lps theme template push', '$ lps theme template push --dry-run'];
|
|
12
15
|
static flags = {
|
|
@@ -17,6 +20,10 @@ export default class Push extends PushCommand {
|
|
|
17
20
|
await this.parse(Push);
|
|
18
21
|
const templatesPath = resolveResourceDir('template', this.localConfig);
|
|
19
22
|
const partsPath = resolveResourceDir('part', this.localConfig);
|
|
23
|
+
// Pushing would only create an empty child theme as a side effect.
|
|
24
|
+
if (!projectHasChildThemeFiles(this.localConfig)) {
|
|
25
|
+
this.error(`Nothing to push: no ${templatesPath}, ${partsPath}, ${resolveResourceDir('css', this.localConfig)} or ${resolveResourceDir('blockCss', this.localConfig)} directory.`);
|
|
26
|
+
}
|
|
20
27
|
this.log(`Pushing templates and parts to ${this.siteConfig.url}`);
|
|
21
28
|
this.log(`Templates path: ${templatesPath}, parts path: ${partsPath}`);
|
|
22
29
|
const { parts, problems, templates } = await readLocalTemplates(templatesPath, partsPath);
|
|
@@ -37,13 +44,25 @@ export default class Push extends PushCommand {
|
|
|
37
44
|
logChanges((message) => { this.log(message); }, changes.templates, 'templates/');
|
|
38
45
|
logChanges((message) => { this.log(message); }, changes.parts, 'parts/');
|
|
39
46
|
logChanges((message) => { this.log(message); }, changes.css, 'css/');
|
|
47
|
+
if (this.localConfig.activateChildTheme) {
|
|
48
|
+
const { child } = (await this.capabilities()).theme;
|
|
49
|
+
if (child && !child.active)
|
|
50
|
+
this.log(`[dry-run] Would activate ${child.stylesheet}.`);
|
|
51
|
+
}
|
|
40
52
|
return { ...names, changes, status: 'dry-run' };
|
|
41
53
|
}
|
|
42
54
|
const child = await this.wp.put(CHILD_THEME_ENDPOINT, { ...css, parts, templates });
|
|
43
55
|
await this.recordSuccess();
|
|
44
56
|
this.log(`Child theme ${child.stylesheet} written.`);
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// Activated only when the project asks for it: a theme switch changes every per-theme setting.
|
|
58
|
+
let { active } = child;
|
|
59
|
+
if (!active && this.localConfig.activateChildTheme) {
|
|
60
|
+
const activation = await activateTheme(this.wp, child.stylesheet);
|
|
61
|
+
active = true;
|
|
62
|
+
this.log(`Activated ${child.stylesheet} (was ${activation.previous}), as loopress.json's activateChildTheme asks.`);
|
|
63
|
+
}
|
|
64
|
+
if (!active) {
|
|
65
|
+
this.warn(`${child.stylesheet} is not the active theme, so nothing changed on the site yet. Run \`lps theme activate\`, or set "activateChildTheme": true in loopress.json.`);
|
|
47
66
|
}
|
|
48
67
|
if (child.customized.length > 0) {
|
|
49
68
|
this.warn(`Edited in the Site Editor, their database copy still wins over the pushed file: ${child.customized.join(', ')}. ` +
|
|
@@ -57,7 +76,7 @@ export default class Push extends PushCommand {
|
|
|
57
76
|
}
|
|
58
77
|
return {
|
|
59
78
|
...names,
|
|
60
|
-
active
|
|
79
|
+
active,
|
|
61
80
|
customized: child.customized,
|
|
62
81
|
status: 'success',
|
|
63
82
|
stylesheet: child.stylesheet,
|
|
@@ -71,7 +90,7 @@ export default class Push extends PushCommand {
|
|
|
71
90
|
child = await this.wp.get(CHILD_THEME_ENDPOINT);
|
|
72
91
|
}
|
|
73
92
|
catch (error) {
|
|
74
|
-
this.error(`The site would refuse this push: ${error
|
|
93
|
+
this.error(`The site would refuse this push: ${errorMessage(error)}`);
|
|
75
94
|
}
|
|
76
95
|
const localCss = new Map(css.blocks.map((block) => [`blocks/${block.name}.css`, sha256(block.css)]));
|
|
77
96
|
if (css.style !== '')
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const CONSOLE_AUTH: z.ZodObject<{
|
|
3
|
+
email: z.ZodOptional<z.ZodString>;
|
|
4
|
+
savedAt: z.ZodString;
|
|
5
|
+
token: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type ConsoleAuth = z.infer<typeof CONSOLE_AUTH>;
|
|
6
8
|
export declare class AuthManager {
|
|
7
|
-
private dataDir
|
|
8
|
-
constructor(dataDir?: string
|
|
9
|
+
private dataDir;
|
|
10
|
+
constructor(dataDir?: string);
|
|
9
11
|
clearAuth(): void;
|
|
10
12
|
getAuth(): ConsoleAuth | null;
|
|
11
13
|
getAuthFilePath(): string;
|
|
@@ -14,3 +16,4 @@ export declare class AuthManager {
|
|
|
14
16
|
private requireDataDir;
|
|
15
17
|
}
|
|
16
18
|
export declare const authManager: AuthManager;
|
|
19
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
5
|
+
const CONSOLE_AUTH = z.object({ email: z.string().optional(), savedAt: z.string(), token: z.string() });
|
|
4
6
|
export class AuthManager {
|
|
5
7
|
dataDir;
|
|
6
8
|
constructor(dataDir) {
|
|
@@ -12,7 +14,9 @@ export class AuthManager {
|
|
|
12
14
|
unlinkSync(filePath);
|
|
13
15
|
}
|
|
14
16
|
getAuth() {
|
|
15
|
-
|
|
17
|
+
// A hand-edited or truncated auth.json reads as logged out, not as a token-less session.
|
|
18
|
+
const result = CONSOLE_AUTH.safeParse(readJsonFile(this.getAuthFilePath()));
|
|
19
|
+
return result.success ? result.data : null;
|
|
16
20
|
}
|
|
17
21
|
getAuthFilePath() {
|
|
18
22
|
return join(this.requireDataDir(), 'auth.json');
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function readJsonFile
|
|
1
|
+
export declare function readJsonFile(filePath: string): unknown;
|
|
2
2
|
export declare function writeJsonFileAtomic(filePath: string, data: unknown): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type EnvironmentConfig, type LoopressConfig, type ProjectConfig } from '../types/config.js';
|
|
2
2
|
export declare class ProjectConfigManager {
|
|
3
|
-
private configDir
|
|
4
|
-
constructor(configDir?: string
|
|
3
|
+
private configDir;
|
|
4
|
+
constructor(configDir?: string);
|
|
5
5
|
createProjectId(name: string): string;
|
|
6
6
|
ensureConfigDir(): void;
|
|
7
7
|
findProjectByApiId(apiProjectId: string): null | (ProjectConfig & {
|