@loopress/cli 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1460 -337
- package/bin/dev.js +0 -0
- package/dist/commands/acf/pull.d.ts +9 -1
- package/dist/commands/acf/pull.js +2 -0
- package/dist/commands/acf/push.d.ts +9 -1
- package/dist/commands/acf/push.js +3 -1
- package/dist/commands/api/publish.js +1 -1
- package/dist/commands/app/push.js +1 -1
- package/dist/commands/composer/push.js +3 -3
- package/dist/commands/cpt/diff.js +5 -0
- package/dist/commands/cpt/list.d.ts +2 -0
- package/dist/commands/cpt/list.js +4 -0
- package/dist/commands/cpt/pull.d.ts +2 -0
- package/dist/commands/cpt/pull.js +4 -0
- package/dist/commands/cpt/push.d.ts +2 -0
- package/dist/commands/cpt/push.js +4 -0
- package/dist/commands/cpt/rm.d.ts +2 -0
- package/dist/commands/cpt/rm.js +3 -0
- package/dist/commands/cpt/rollback.js +5 -0
- package/dist/commands/dev.js +14 -4
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +30 -2
- package/dist/commands/form/pull.d.ts +9 -1
- package/dist/commands/form/pull.js +3 -1
- package/dist/commands/form/push.d.ts +7 -1
- package/dist/commands/form/push.js +3 -1
- package/dist/commands/init.js +6 -38
- package/dist/commands/menu/pull.d.ts +7 -1
- package/dist/commands/menu/pull.js +2 -0
- package/dist/commands/menu/push.d.ts +7 -1
- package/dist/commands/menu/push.js +3 -1
- package/dist/commands/meta/diff.d.ts +2 -0
- package/dist/commands/meta/diff.js +5 -0
- package/dist/commands/meta/list.d.ts +19 -0
- package/dist/commands/meta/list.js +21 -0
- package/dist/commands/meta/pull.d.ts +2 -0
- package/dist/commands/meta/pull.js +4 -0
- package/dist/commands/meta/push.d.ts +2 -0
- package/dist/commands/meta/push.js +4 -0
- package/dist/commands/meta/rm.d.ts +2 -0
- package/dist/commands/meta/rm.js +3 -0
- package/dist/commands/meta/rollback.d.ts +2 -0
- package/dist/commands/meta/rollback.js +5 -0
- package/dist/commands/option/list.js +2 -8
- package/dist/commands/option/pull.js +4 -4
- package/dist/commands/option/push.js +2 -0
- package/dist/commands/page/push.js +3 -0
- package/dist/commands/pattern/diff.d.ts +2 -0
- package/dist/commands/pattern/diff.js +5 -0
- package/dist/commands/pattern/list.d.ts +2 -0
- package/dist/commands/pattern/list.js +3 -0
- package/dist/commands/pattern/pull.d.ts +2 -0
- package/dist/commands/pattern/pull.js +4 -0
- package/dist/commands/pattern/push.d.ts +2 -0
- package/dist/commands/pattern/push.js +4 -0
- package/dist/commands/pattern/rm.d.ts +2 -0
- package/dist/commands/pattern/rm.js +3 -0
- package/dist/commands/pattern/rollback.d.ts +2 -0
- package/dist/commands/pattern/rollback.js +5 -0
- package/dist/commands/plugin/audit.js +2 -1
- package/dist/commands/plugin/pull.d.ts +4 -0
- package/dist/commands/plugin/pull.js +56 -10
- package/dist/commands/plugin/push.js +5 -4
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +20 -1
- package/dist/commands/project/list.d.ts +1 -1
- package/dist/commands/project/list.js +6 -6
- package/dist/commands/project/pull.js +11 -8
- package/dist/commands/project/push.js +23 -18
- package/dist/commands/project/remove.js +1 -1
- package/dist/commands/project/switch.d.ts +0 -1
- package/dist/commands/project/switch.js +16 -36
- package/dist/commands/pull.js +7 -2
- package/dist/commands/push.js +14 -6
- package/dist/commands/role/diff.d.ts +2 -0
- package/dist/commands/role/diff.js +5 -0
- package/dist/commands/role/list.d.ts +2 -0
- package/dist/commands/role/list.js +4 -0
- package/dist/commands/role/pull.d.ts +2 -0
- package/dist/commands/role/pull.js +4 -0
- package/dist/commands/role/push.d.ts +2 -0
- package/dist/commands/role/push.js +4 -0
- package/dist/commands/role/rm.d.ts +2 -0
- package/dist/commands/role/rm.js +3 -0
- package/dist/commands/role/rollback.d.ts +2 -0
- package/dist/commands/role/rollback.js +5 -0
- package/dist/commands/snippet/publish.js +1 -1
- package/dist/commands/snippet/pull.js +1 -1
- package/dist/commands/snippet/push.js +1 -1
- package/dist/commands/status.d.ts +3 -3
- package/dist/commands/status.js +21 -53
- package/dist/commands/taxonomy/diff.d.ts +2 -0
- package/dist/commands/taxonomy/diff.js +5 -0
- package/dist/commands/taxonomy/list.d.ts +2 -0
- package/dist/commands/taxonomy/list.js +5 -0
- package/dist/commands/taxonomy/pull.d.ts +2 -0
- package/dist/commands/taxonomy/pull.js +4 -0
- package/dist/commands/taxonomy/push.d.ts +2 -0
- package/dist/commands/taxonomy/push.js +4 -0
- package/dist/commands/taxonomy/rm.d.ts +2 -0
- package/dist/commands/taxonomy/rm.js +3 -0
- package/dist/commands/taxonomy/rollback.d.ts +2 -0
- package/dist/commands/taxonomy/rollback.js +5 -0
- package/dist/commands/theme/pull.d.ts +6 -10
- package/dist/commands/theme/pull.js +30 -32
- package/dist/commands/theme/push.d.ts +8 -9
- package/dist/commands/theme/push.js +54 -111
- package/dist/commands/theme/style/pull.d.ts +7 -1
- package/dist/commands/theme/style/pull.js +3 -1
- package/dist/commands/theme/style/push.d.ts +8 -1
- package/dist/commands/theme/style/push.js +4 -1
- package/dist/commands/{template → theme/template}/diff.d.ts +1 -1
- package/dist/commands/{template → theme/template}/diff.js +3 -3
- package/dist/commands/{template → theme/template}/list.d.ts +2 -2
- package/dist/commands/{template → theme/template}/list.js +5 -5
- package/dist/commands/{template → theme/template}/push.d.ts +3 -1
- package/dist/commands/theme/template/push.js +65 -0
- package/dist/commands/theme/version/pull.d.ts +23 -0
- package/dist/commands/theme/version/pull.js +43 -0
- package/dist/commands/theme/version/push.d.ts +22 -0
- package/dist/commands/theme/version/push.js +123 -0
- package/dist/config/project-config.manager.d.ts +3 -3
- package/dist/config/project-config.manager.js +22 -31
- package/dist/hooks/finally.d.ts +2 -1
- package/dist/hooks/finally.js +38 -7
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/app-build.js +3 -1
- package/dist/lib/app-manifest.js +2 -2
- package/dist/lib/base.d.ts +2 -2
- package/dist/lib/base.js +33 -38
- package/dist/lib/declared-type-command.d.ts +35 -0
- package/dist/lib/declared-type-command.js +192 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +7 -3
- package/dist/lib/diff-state.js +1 -0
- package/dist/lib/github-release.js +4 -1
- package/dist/lib/local-callback-server.d.ts +12 -6
- package/dist/lib/local-callback-server.js +24 -23
- package/dist/lib/php-files-command.js +3 -1
- package/dist/lib/push-command.d.ts +6 -0
- package/dist/lib/push-command.js +61 -18
- package/dist/lib/put-or-create.js +3 -4
- package/dist/lib/resource-state.js +40 -86
- package/dist/lib/rollback-materialize.js +13 -24
- package/dist/lib/snapshot-store.js +2 -2
- package/dist/lib/spinner.d.ts +2 -0
- package/dist/lib/spinner.js +72 -0
- package/dist/lib/temp-admin.d.ts +10 -1
- package/dist/lib/temp-admin.js +27 -2
- package/dist/lib/validate-local.js +31 -9
- package/dist/lib/wp-authorize-flow.d.ts +10 -17
- package/dist/lib/wp-authorize-flow.js +24 -28
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +7 -1
- package/dist/lib/wp-site-diagnostic.js +39 -1
- package/dist/types/global-config.generated.d.ts +1 -5
- package/dist/types/project-config.generated.d.ts +28 -4
- package/dist/utils/composer.d.ts +3 -1
- package/dist/utils/composer.js +16 -7
- package/dist/utils/declared-type-format.d.ts +52 -0
- package/dist/utils/declared-type-format.js +233 -0
- package/dist/utils/option-format.d.ts +3 -1
- package/dist/utils/option-format.js +14 -3
- package/dist/utils/page-format.d.ts +5 -1
- package/dist/utils/page-format.js +7 -3
- package/dist/utils/plugin-sync.js +1 -1
- package/dist/utils/resource-dirs.d.ts +9 -3
- package/dist/utils/resource-dirs.js +18 -6
- package/dist/utils/template-format.d.ts +3 -1
- package/dist/utils/template-format.js +10 -3
- package/dist/utils/theme-css.d.ts +19 -0
- package/dist/utils/theme-css.js +100 -0
- package/oclif.manifest.json +3542 -1087
- package/package.json +40 -21
- package/schemas/global-config.schema.json +2 -6
- package/schemas/project-config.schema.json +37 -7
- package/dist/commands/seo/diff.js +0 -5
- package/dist/commands/seo/list.d.ts +0 -18
- package/dist/commands/seo/list.js +0 -58
- package/dist/commands/seo/pull.d.ts +0 -17
- package/dist/commands/seo/pull.js +0 -87
- package/dist/commands/seo/push.d.ts +0 -23
- package/dist/commands/seo/push.js +0 -176
- package/dist/commands/seo/rollback.js +0 -5
- package/dist/commands/template/push.js +0 -42
- package/dist/utils/seo-format.d.ts +0 -29
- package/dist/utils/seo-format.js +0 -18
- /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
- /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileExtension, formatDeclaredFile, itemEndpoint, parseDeclaredFile, } from '../utils/declared-type-format.js';
|
|
6
|
+
import { pluralize } from '../utils/pluralize.js';
|
|
7
|
+
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
8
|
+
import { LoopressCommand } from './base.js';
|
|
9
|
+
import { basenameKey, findOrphanedFiles } from './find-orphaned-files.js';
|
|
10
|
+
import { isInteractive } from './interactive.js';
|
|
11
|
+
import { loadFiles } from './load-files.js';
|
|
12
|
+
import { PushCommand } from './push-command.js';
|
|
13
|
+
import { getResourceStateProvider } from './resource-state.js';
|
|
14
|
+
import { isApplicative404, isNotFoundError } from './wp-client.js';
|
|
15
|
+
const SOURCE_LABELS = {
|
|
16
|
+
acf: 'ACF',
|
|
17
|
+
cptui: 'CPT UI',
|
|
18
|
+
loopress: 'Loopress',
|
|
19
|
+
other: 'theme or plugin',
|
|
20
|
+
wordpress: 'WordPress',
|
|
21
|
+
};
|
|
22
|
+
function pathArg(spec) {
|
|
23
|
+
return Args.string({ description: `Path to the ${spec.dir} directory (overrides project config)` });
|
|
24
|
+
}
|
|
25
|
+
export function declaredPushCommand(spec, description) {
|
|
26
|
+
class DeclaredPush extends PushCommand {
|
|
27
|
+
static args = { path: pathArg(spec) };
|
|
28
|
+
static description = description;
|
|
29
|
+
static enableJsonFlag = true;
|
|
30
|
+
static examples = [`$ lps ${spec.cliName} push`];
|
|
31
|
+
static flags = { ...PushCommand.dryRunFlag, ...PushCommand.yesFlag };
|
|
32
|
+
async run() {
|
|
33
|
+
const { args } = await this.parse(DeclaredPush);
|
|
34
|
+
const path = resolveResourceDir(spec.dirKind, this.localConfig, args.path);
|
|
35
|
+
this.log(`Pushing ${spec.plural} to ${this.siteConfig.url}`);
|
|
36
|
+
this.log(`Path: ${path}`);
|
|
37
|
+
const provider = getResourceStateProvider(spec.cliName);
|
|
38
|
+
const beforeState = await this.captureBeforePushState(provider, path);
|
|
39
|
+
const items = await loadFiles(path, {
|
|
40
|
+
extension: fileExtension(spec),
|
|
41
|
+
onSkip: (message) => {
|
|
42
|
+
this.warn(message);
|
|
43
|
+
},
|
|
44
|
+
parse: (raw, filePath) => parseDeclaredFile(spec, filePath, raw),
|
|
45
|
+
});
|
|
46
|
+
this.log(`Found ${pluralize(items.length, spec.noun, spec.plural)} to push`);
|
|
47
|
+
const pushed = [];
|
|
48
|
+
await this.runPushTasks(items, (item) => item.slug, async (item, task) => {
|
|
49
|
+
await this.pushItem(item, task);
|
|
50
|
+
pushed.push(item.slug);
|
|
51
|
+
});
|
|
52
|
+
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
53
|
+
if (this.failedCount > 0) {
|
|
54
|
+
this.error(`${pluralize(this.failedCount, spec.noun, spec.plural)} failed to push.`);
|
|
55
|
+
}
|
|
56
|
+
if (this.dryRun)
|
|
57
|
+
return { path, pushed, status: 'dry-run' };
|
|
58
|
+
await this.recordSuccess();
|
|
59
|
+
this.log(`All ${spec.plural} pushed.`);
|
|
60
|
+
return { path, pushed, status: 'success' };
|
|
61
|
+
}
|
|
62
|
+
async currentRevision(slug) {
|
|
63
|
+
try {
|
|
64
|
+
return (await this.wp.get(itemEndpoint(spec, slug))).revision;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (isNotFoundError(error))
|
|
68
|
+
return undefined;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async pushItem({ args, slug }, task) {
|
|
73
|
+
if (this.dryRun) {
|
|
74
|
+
if (task)
|
|
75
|
+
task.output = `[dry-run] Would push: ${slug}`;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// Conditional write (#234), same as `menu push`: WordPress refuses it (412) if the item
|
|
80
|
+
// changed since this read. Slug, reserved names and code arguments are checked there.
|
|
81
|
+
const expectedRevision = await this.currentRevision(slug);
|
|
82
|
+
const body = { args, slug };
|
|
83
|
+
if (expectedRevision !== undefined)
|
|
84
|
+
body.expectedRevision = expectedRevision;
|
|
85
|
+
await this.wp.post(spec.endpoint, body);
|
|
86
|
+
if (task)
|
|
87
|
+
task.output = `Pushed: ${slug}`;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.reportTaskFailure(`Failed to push ${slug}: ${error.message}`, error, task);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return DeclaredPush;
|
|
95
|
+
}
|
|
96
|
+
export function declaredPullCommand(spec, description) {
|
|
97
|
+
class DeclaredPull extends LoopressCommand {
|
|
98
|
+
static args = { path: pathArg(spec) };
|
|
99
|
+
static description = description;
|
|
100
|
+
static enableJsonFlag = true;
|
|
101
|
+
static examples = [`$ lps ${spec.cliName} pull`];
|
|
102
|
+
static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
|
|
103
|
+
async run() {
|
|
104
|
+
const { args } = await this.parse(DeclaredPull);
|
|
105
|
+
const path = resolveResourceDir(spec.dirKind, this.localConfig, args.path);
|
|
106
|
+
this.log(`Pulling ${spec.plural} from ${this.siteConfig.url}`);
|
|
107
|
+
this.log(`Path: ${path}`);
|
|
108
|
+
const remote = await this.wp.get(spec.endpoint);
|
|
109
|
+
const orphans = await findOrphanedFiles(path, new Set(remote.map(({ slug }) => slug)), { extensions: [fileExtension(spec)], key: basenameKey });
|
|
110
|
+
await this.pullDirectory(path, remote, orphans, {
|
|
111
|
+
dryRunMessage: `Would pull ${pluralize(remote.length, spec.noun, spec.plural)} to ${path}`,
|
|
112
|
+
orphanReason: `in ${path} no longer present on WordPress`,
|
|
113
|
+
pulledMessage: `Pulled ${pluralize(remote.length, spec.noun, spec.plural)} to ${path}`,
|
|
114
|
+
title: ({ slug }) => slug,
|
|
115
|
+
// Only the arguments: the slug is the file name, the revision is push bookkeeping.
|
|
116
|
+
async write(item, dir) {
|
|
117
|
+
await writeFile(join(dir, `${item.slug}${fileExtension(spec)}`), formatDeclaredFile(spec, item.args));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return { path, status: this.dryRun ? 'dry-run' : 'success' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return DeclaredPull;
|
|
124
|
+
}
|
|
125
|
+
// `countLabel` renders the item's count: "3 published" for a post type, "3 terms" for a taxonomy,
|
|
126
|
+
// nothing when it returns an empty string.
|
|
127
|
+
export function declaredListCommand(spec, description, countLabel) {
|
|
128
|
+
class DeclaredList extends LoopressCommand {
|
|
129
|
+
static description = description;
|
|
130
|
+
static enableJsonFlag = true;
|
|
131
|
+
static examples = [`$ lps ${spec.cliName} list`];
|
|
132
|
+
async run() {
|
|
133
|
+
const items = await this.wp.get(spec.registeredEndpoint);
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const attached = item.objectTypes && item.objectTypes.length > 0 ? `, on ${item.objectTypes.join(', ')}` : '';
|
|
136
|
+
const count = countLabel(item.count);
|
|
137
|
+
const counted = count ? ', ' + count : '';
|
|
138
|
+
this.log(`${item.slug} (${item.label}): ${SOURCE_LABELS[item.source]}${counted}${attached}`);
|
|
139
|
+
if (item.conflict) {
|
|
140
|
+
this.warn(`${item.slug}: declared in ${spec.dir}/ but already registered by ${SOURCE_LABELS[item.source]}, the Loopress version is skipped`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return items;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return DeclaredList;
|
|
147
|
+
}
|
|
148
|
+
// The only way a Loopress item leaves WordPress: push never deletes (see Push Deletion Rules in
|
|
149
|
+
// the product docs). The local file is left alone, same as `hook rm`. `keptNote` says what
|
|
150
|
+
// happens to what used it ("Its posts stay in the database, hidden until it's pushed again.").
|
|
151
|
+
export function declaredRmCommand(spec, description, keptNote) {
|
|
152
|
+
class DeclaredRm extends LoopressCommand {
|
|
153
|
+
static aliases = [`${spec.cliName}:remove`];
|
|
154
|
+
static args = {
|
|
155
|
+
slug: Args.string({ description: `The ${spec.noun} slug, its ${spec.dir}/ file name without ${fileExtension(spec)}`, required: true }),
|
|
156
|
+
};
|
|
157
|
+
static description = description;
|
|
158
|
+
static enableJsonFlag = true;
|
|
159
|
+
static examples = [`$ lps ${spec.cliName} rm <slug>`, `$ lps ${spec.cliName} remove <slug> --yes`];
|
|
160
|
+
static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
|
|
161
|
+
async run() {
|
|
162
|
+
const { args } = await this.parse(DeclaredRm);
|
|
163
|
+
const { slug } = args;
|
|
164
|
+
const { url } = this.siteConfig;
|
|
165
|
+
if (this.dryRun) {
|
|
166
|
+
this.log(`[dry-run] Would remove ${spec.noun} ${slug} from ${url}`);
|
|
167
|
+
return { removed: false, slug, status: 'dry-run' };
|
|
168
|
+
}
|
|
169
|
+
if (!this.yes) {
|
|
170
|
+
if (!isInteractive())
|
|
171
|
+
this.error(`Removing ${spec.noun} ${slug} needs confirmation. Re-run with --yes.`);
|
|
172
|
+
const ok = await confirm({ default: false, message: `Remove ${spec.noun} ${slug} from ${url}? ${keptNote}` });
|
|
173
|
+
if (!ok) {
|
|
174
|
+
this.log('Aborted.');
|
|
175
|
+
return { removed: false, slug, status: 'aborted' };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await this.wp.delete(itemEndpoint(spec, slug));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const notFound = `${spec.noun[0].toUpperCase()}${spec.noun.slice(1)} not found`;
|
|
183
|
+
if (isApplicative404(error, notFound))
|
|
184
|
+
this.error(`${spec.noun} ${slug} is not managed by Loopress on ${url}.`);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
this.log(`Removed ${spec.noun} ${slug} from ${url}. ${keptNote}`);
|
|
188
|
+
return { removed: true, slug, status: 'success' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return DeclaredRm;
|
|
192
|
+
}
|
package/dist/lib/dev-watch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
2
|
-
export type ResourceType = 'api' | 'hooks' | 'pages' | 'parts' | 'plugins' | 'snippets' | 'templates';
|
|
2
|
+
export type ResourceType = 'api' | 'blocks' | 'css' | 'hooks' | 'pages' | 'parts' | 'plugins' | 'snippets' | 'templates';
|
|
3
3
|
export declare const RESOURCE_TYPES: ResourceType[];
|
|
4
4
|
export type WatchTarget = {
|
|
5
5
|
commandId: string;
|
package/dist/lib/dev-watch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, sep } from 'node:path';
|
|
3
|
-
|
|
3
|
+
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
4
|
+
export const RESOURCE_TYPES = ['snippets', 'templates', 'parts', 'css', 'blocks', 'pages', 'api', 'hooks', 'plugins'];
|
|
4
5
|
// `only` narrows the default set, `skip` then removes from whatever that set is, same
|
|
5
6
|
// precedence a combined `--only=a,b --skip=b` reads as: watch a, b, then drop b.
|
|
6
7
|
export function resolveResourceTypes(only, skip = []) {
|
|
@@ -15,11 +16,14 @@ export function buildWatchTargets(types, localConfig, cwd) {
|
|
|
15
16
|
const rootDir = join(cwd, localConfig.rootDir ?? '.');
|
|
16
17
|
const candidates = {
|
|
17
18
|
api: { commandId: 'api:push', path: join(rootDir, localConfig.apiDir ?? 'api'), type: 'api' },
|
|
19
|
+
// The theme CSS is bundled into the child theme with the templates, by the same push.
|
|
20
|
+
blocks: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('blockCss', localConfig)), type: 'blocks' },
|
|
21
|
+
css: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('css', localConfig)), type: 'css' },
|
|
18
22
|
hooks: { commandId: 'hook:push', path: join(rootDir, localConfig.hooksDir ?? 'hooks'), type: 'hooks' },
|
|
19
23
|
pages: { commandId: 'page:push', path: join(rootDir, localConfig.pageDir ?? 'pages'), type: 'pages' },
|
|
20
24
|
plugins: { commandId: 'plugin:push', path: join(cwd, 'loopress.json'), type: 'plugins' },
|
|
21
|
-
parts: { commandId: 'template:push', path: join(
|
|
22
|
-
templates: { commandId: 'template:push', path: join(
|
|
25
|
+
parts: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('part', localConfig)), type: 'parts' },
|
|
26
|
+
templates: { commandId: 'theme:template:push', path: join(cwd, resolveResourceDir('template', localConfig)), type: 'templates' },
|
|
23
27
|
snippets: { commandId: 'snippet:push', path: join(rootDir, localConfig.snippetsDir ?? 'snippets'), type: 'snippets' },
|
|
24
28
|
};
|
|
25
29
|
return types
|
package/dist/lib/diff-state.js
CHANGED
|
@@ -29,6 +29,7 @@ function renderTextChange(id, left, right, labels) {
|
|
|
29
29
|
function objectFieldChanges(left, right) {
|
|
30
30
|
return microdiff(left, right, { cyclesFix: false }).map((change) => {
|
|
31
31
|
const path = change.path.join('.');
|
|
32
|
+
// microdiff types leaf values as `any`.
|
|
32
33
|
if (change.type === 'CREATE')
|
|
33
34
|
return { kind: 'added', path, to: change.value };
|
|
34
35
|
if (change.type === 'REMOVE')
|
|
@@ -4,8 +4,8 @@ import { mkdtemp } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { withSpinner } from './spinner.js';
|
|
7
8
|
const REPO = 'loopress/loopress';
|
|
8
|
-
const RELEASE_TAG_PATTERN = /^wordpress-plugin@/;
|
|
9
9
|
const ASSET_NAME = 'loopress-full.zip';
|
|
10
10
|
/**
|
|
11
11
|
* Downloads `loopress-full.zip` from the most recent `wordpress-plugin@*` release of
|
|
@@ -18,6 +18,9 @@ const ASSET_NAME = 'loopress-full.zip';
|
|
|
18
18
|
* "the one zip asset".
|
|
19
19
|
*/
|
|
20
20
|
export async function downloadLatestFullZip() {
|
|
21
|
+
return withSpinner('Downloading Loopress Full', download);
|
|
22
|
+
}
|
|
23
|
+
async function download() {
|
|
21
24
|
const releases = await got(`https://api.github.com/repos/${REPO}/releases`, {
|
|
22
25
|
headers: { accept: 'application/vnd.github+json', 'user-agent': 'loopress-cli' },
|
|
23
26
|
searchParams: { 'per_page': 30 },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export type CallbackHelpers<T> = {
|
|
2
|
-
body: Record<string, string>;
|
|
3
2
|
rejectWithPage: (page: string, error: Error) => void;
|
|
4
3
|
resolveWithPage: (page: string, value: T) => void;
|
|
5
4
|
respondBadRequest: (message: string) => void;
|
|
@@ -9,13 +8,14 @@ export type CallbackHelpers<T> = {
|
|
|
9
8
|
* local server; this factors out the server setup, timeout, and browser-opening boilerplate.
|
|
10
9
|
*
|
|
11
10
|
* The loopback server is unauthenticated, so any local process or web page open during the wait
|
|
12
|
-
* could otherwise
|
|
11
|
+
* could otherwise send forged credentials to it (RFC 8252 section 8.9). Two guards close that:
|
|
13
12
|
*
|
|
14
13
|
* - a 32-byte `state` generated here, threaded into the authorize URL by `buildUrl`, and required
|
|
15
|
-
* back (constant-time compare) on any request
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `Origin
|
|
14
|
+
* back (constant-time compare) on any request with a query string, the only place credentials
|
|
15
|
+
* are read from;
|
|
16
|
+
* - an `Origin` allowlist: the legitimate callbacks are top-level redirects, which send no
|
|
17
|
+
* `Origin`, so a request from a page script (carrying its own origin) is rejected. That's
|
|
18
|
+
* defense in depth, `state` is the primary control.
|
|
19
19
|
*
|
|
20
20
|
* The first request that looks like a callback (valid or not) shuts the server down, so a
|
|
21
21
|
* failed guess gets no second try within the window.
|
|
@@ -23,12 +23,18 @@ export type CallbackHelpers<T> = {
|
|
|
23
23
|
export declare function waitForLocalCallback<T>(options: {
|
|
24
24
|
allowedOrigins: string[];
|
|
25
25
|
buildUrl: (callbackBaseUrl: string, state: string) => string;
|
|
26
|
+
/** Host advertised in the callback URL; the server itself always listens on 127.0.0.1. */
|
|
27
|
+
callbackHost?: string;
|
|
26
28
|
handleRequest: (url: URL, helpers: CallbackHelpers<T>) => void;
|
|
27
29
|
log: (message: string) => void;
|
|
28
30
|
openingMessage: string;
|
|
29
31
|
timeoutMessage: string;
|
|
30
32
|
timeoutMs?: number;
|
|
31
33
|
}): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* The page rewrites its own URL to `/` on load, so the callback query string (an Application
|
|
36
|
+
* Password, a console token) doesn't stay in the browser history or address bar.
|
|
37
|
+
*/
|
|
32
38
|
export declare function renderResultPage(options: {
|
|
33
39
|
background: string;
|
|
34
40
|
heading: string;
|
|
@@ -2,15 +2,7 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
4
|
import { openBrowser } from './open-browser.js';
|
|
5
|
-
|
|
6
|
-
const chunks = [];
|
|
7
|
-
for await (const chunk of req)
|
|
8
|
-
chunks.push(chunk);
|
|
9
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
10
|
-
}
|
|
11
|
-
function parseFormData(body) {
|
|
12
|
-
return Object.fromEntries(new URLSearchParams(body));
|
|
13
|
-
}
|
|
5
|
+
import { withSpinner } from './spinner.js';
|
|
14
6
|
/** Constant-time compare that also tolerates a length mismatch without throwing. */
|
|
15
7
|
function safeEqual(a, b) {
|
|
16
8
|
const bufferA = Buffer.from(a);
|
|
@@ -22,13 +14,14 @@ function safeEqual(a, b) {
|
|
|
22
14
|
* local server; this factors out the server setup, timeout, and browser-opening boilerplate.
|
|
23
15
|
*
|
|
24
16
|
* The loopback server is unauthenticated, so any local process or web page open during the wait
|
|
25
|
-
* could otherwise
|
|
17
|
+
* could otherwise send forged credentials to it (RFC 8252 section 8.9). Two guards close that:
|
|
26
18
|
*
|
|
27
19
|
* - a 32-byte `state` generated here, threaded into the authorize URL by `buildUrl`, and required
|
|
28
|
-
* back (constant-time compare) on any request
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `Origin
|
|
20
|
+
* back (constant-time compare) on any request with a query string, the only place credentials
|
|
21
|
+
* are read from;
|
|
22
|
+
* - an `Origin` allowlist: the legitimate callbacks are top-level redirects, which send no
|
|
23
|
+
* `Origin`, so a request from a page script (carrying its own origin) is rejected. That's
|
|
24
|
+
* defense in depth, `state` is the primary control.
|
|
32
25
|
*
|
|
33
26
|
* The first request that looks like a callback (valid or not) shuts the server down, so a
|
|
34
27
|
* failed guess gets no second try within the window.
|
|
@@ -36,25 +29,28 @@ function safeEqual(a, b) {
|
|
|
36
29
|
export async function waitForLocalCallback(options) {
|
|
37
30
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
38
31
|
const state = randomBytes(32).toString('hex');
|
|
39
|
-
|
|
32
|
+
const callback = new Promise((resolve, reject) => {
|
|
40
33
|
function finish(res, page) {
|
|
41
|
-
|
|
34
|
+
// `Connection: close` (here and below): server.close() only refuses new connections, a
|
|
35
|
+
// kept-alive socket would otherwise still reach this handler after the shutdown.
|
|
36
|
+
res.writeHead(200, { Connection: 'close', 'Content-Type': 'text/html; charset=utf-8' });
|
|
42
37
|
res.end(page);
|
|
43
38
|
clearTimeout(timer);
|
|
44
39
|
server.close();
|
|
45
40
|
}
|
|
46
41
|
function rejectRequest(res, message) {
|
|
47
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
42
|
+
res.writeHead(403, { Connection: 'close', 'Content-Type': 'text/plain' });
|
|
48
43
|
res.end(message);
|
|
49
44
|
clearTimeout(timer);
|
|
50
45
|
server.close();
|
|
51
46
|
reject(new Error(message));
|
|
52
47
|
}
|
|
53
|
-
|
|
48
|
+
function handleIncoming(req, res) {
|
|
54
49
|
try {
|
|
55
50
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
// Any query string counts, not just one carrying `state`: credentials are read from the
|
|
52
|
+
// query, so a request with `password=` but no `state` must not slip past the check.
|
|
53
|
+
const looksLikeCallback = url.search !== '';
|
|
58
54
|
if (looksLikeCallback) {
|
|
59
55
|
const { origin } = req.headers;
|
|
60
56
|
if (origin !== undefined && !options.allowedOrigins.includes(origin)) {
|
|
@@ -79,7 +75,6 @@ export async function waitForLocalCallback(options) {
|
|
|
79
75
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
80
76
|
res.end(message);
|
|
81
77
|
},
|
|
82
|
-
body,
|
|
83
78
|
});
|
|
84
79
|
}
|
|
85
80
|
catch (error) {
|
|
@@ -89,7 +84,7 @@ export async function waitForLocalCallback(options) {
|
|
|
89
84
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
|
-
const server = createServer(
|
|
87
|
+
const server = createServer(handleIncoming);
|
|
93
88
|
server.on('error', (err) => {
|
|
94
89
|
clearTimeout(timer);
|
|
95
90
|
reject(err);
|
|
@@ -100,13 +95,18 @@ export async function waitForLocalCallback(options) {
|
|
|
100
95
|
}, timeoutMs);
|
|
101
96
|
server.listen(0, '127.0.0.1', () => {
|
|
102
97
|
const { port } = server.address();
|
|
103
|
-
const targetUrl = options.buildUrl(`http
|
|
98
|
+
const targetUrl = options.buildUrl(`http://${options.callbackHost ?? 'localhost'}:${port}`, state);
|
|
104
99
|
options.log(options.openingMessage);
|
|
105
100
|
options.log(`\nIf it doesn't open automatically, visit:\n${targetUrl}\n`);
|
|
106
101
|
openBrowser(targetUrl);
|
|
107
102
|
});
|
|
108
103
|
});
|
|
104
|
+
return withSpinner('Waiting for the browser', async () => callback);
|
|
109
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The page rewrites its own URL to `/` on load, so the callback query string (an Application
|
|
108
|
+
* Password, a console token) doesn't stay in the browser history or address bar.
|
|
109
|
+
*/
|
|
110
110
|
export function renderResultPage(options) {
|
|
111
111
|
return `<!DOCTYPE html>
|
|
112
112
|
<html lang="en">
|
|
@@ -144,6 +144,7 @@ export function renderResultPage(options) {
|
|
|
144
144
|
<h1>${options.heading}</h1>
|
|
145
145
|
<p>You can close this tab and return to your terminal.</p>
|
|
146
146
|
</div>
|
|
147
|
+
<script>history.replaceState(null, '', '/')</script>
|
|
147
148
|
</body>
|
|
148
149
|
</html>`;
|
|
149
150
|
}
|
|
@@ -274,6 +274,8 @@ export function resourceListCommand(spec) {
|
|
|
274
274
|
}
|
|
275
275
|
export function resourceRmCommand(spec) {
|
|
276
276
|
class ResourceRm extends LoopressCommand {
|
|
277
|
+
// `remove` matches `lps app remove` and `lps option remove`, `rm` stays for existing scripts.
|
|
278
|
+
static aliases = [`${spec.cliName}:remove`];
|
|
277
279
|
static args = {
|
|
278
280
|
filename: Args.string({
|
|
279
281
|
description: `The ${spec.noun} to remove, its slug without the .php extension (e.g. "hello" or "invoice-pdf/[order_id]")`,
|
|
@@ -282,7 +284,7 @@ export function resourceRmCommand(spec) {
|
|
|
282
284
|
};
|
|
283
285
|
static description = spec.rmDescription;
|
|
284
286
|
static enableJsonFlag = true;
|
|
285
|
-
static examples = [`$ lps ${spec.cliName} rm hello`, `$ lps ${spec.cliName}
|
|
287
|
+
static examples = [`$ lps ${spec.cliName} rm hello`, `$ lps ${spec.cliName} remove hello --yes`];
|
|
286
288
|
static flags = { ...LoopressCommand.dryRunFlag, ...LoopressCommand.yesFlag };
|
|
287
289
|
async run() {
|
|
288
290
|
const { args } = await this.parse(ResourceRm);
|
|
@@ -2,14 +2,20 @@ import { LoopressCommand } from './base.js';
|
|
|
2
2
|
import { type ResourceState } from './diff-state.js';
|
|
3
3
|
import { type ResourceStateProvider } from './resource-state.js';
|
|
4
4
|
export declare abstract class PushCommand extends LoopressCommand {
|
|
5
|
+
static baseFlags: {
|
|
6
|
+
'production-confirmed': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
5
9
|
protected failedCount: number;
|
|
6
10
|
private refusedByGuard;
|
|
7
11
|
protected captureBeforePushState(provider: ResourceStateProvider, dir: string): Promise<ResourceState | undefined>;
|
|
8
12
|
catch(err: Error): Promise<void>;
|
|
9
13
|
protected guardProductionPush(): Promise<void>;
|
|
10
14
|
init(): Promise<void>;
|
|
15
|
+
protected previewRemote(provider: ResourceStateProvider, dir: string): Promise<void>;
|
|
11
16
|
protected recordDeployment(status: 'failure' | 'success'): Promise<void>;
|
|
12
17
|
protected recordSuccess(): Promise<void>;
|
|
18
|
+
protected reportPushToSite(): Promise<void>;
|
|
13
19
|
protected reportTaskFailure(message: string, error: unknown, task?: {
|
|
14
20
|
output: string;
|
|
15
21
|
}): never;
|
package/dist/lib/push-command.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import got from 'got';
|
|
2
3
|
import { Listr } from 'listr2';
|
|
3
4
|
import { resolve } from 'node:path';
|
|
@@ -6,20 +7,30 @@ import { API_URL } from './api-client.js';
|
|
|
6
7
|
import { LoopressCommand } from './base.js';
|
|
7
8
|
import { guardProductionPush } from './guard-production-push.js';
|
|
8
9
|
import { writeSnapshot } from './snapshot-store.js';
|
|
10
|
+
import { withoutSpinner, withSpinner } from './spinner.js';
|
|
9
11
|
export class PushCommand extends LoopressCommand {
|
|
12
|
+
// Passed by an aggregate command (`lps theme push`) that already confirmed production once,
|
|
13
|
+
// so its steps skip that prompt while still asking their own confirmations (uninstalls).
|
|
14
|
+
static baseFlags = {
|
|
15
|
+
...LoopressCommand.baseFlags,
|
|
16
|
+
'production-confirmed': Flags.boolean({ hidden: true }),
|
|
17
|
+
};
|
|
10
18
|
failedCount = 0;
|
|
11
19
|
// Refusals of the production guard never reach the site, so they must not pollute the
|
|
12
20
|
// deployment history with failure records.
|
|
13
21
|
refusedByGuard = false;
|
|
14
22
|
// Reads `provider`'s remote state right before a real push, for `writeAfterPushSnapshot` to
|
|
15
|
-
// pair with the actual post-push remote state once the push completes.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
23
|
+
// pair with the actual post-push remote state once the push completes. Best-effort: a read
|
|
24
|
+
// failure (the site briefly unreachable) must never block the push itself, the same "never
|
|
25
|
+
// interrupt the push flow" stance recordDeployment takes; `undefined` tells
|
|
26
|
+
// `writeAfterPushSnapshot` there is no restore point to complete.
|
|
27
|
+
//
|
|
28
|
+
// On a dry run, the same read is the preview's check against the site (see previewRemote).
|
|
20
29
|
async captureBeforePushState(provider, dir) {
|
|
21
|
-
if (this.dryRun)
|
|
30
|
+
if (this.dryRun) {
|
|
31
|
+
await this.previewRemote(provider, dir);
|
|
22
32
|
return undefined;
|
|
33
|
+
}
|
|
23
34
|
try {
|
|
24
35
|
return await provider.remote(this.wp, (message) => {
|
|
25
36
|
this.warn(message);
|
|
@@ -34,7 +45,7 @@ export class PushCommand extends LoopressCommand {
|
|
|
34
45
|
if (!this.dryRun && !this.refusedByGuard && this.siteConfig) {
|
|
35
46
|
await this.recordDeployment('failure');
|
|
36
47
|
}
|
|
37
|
-
|
|
48
|
+
await super.catch(err);
|
|
38
49
|
}
|
|
39
50
|
async guardProductionPush() {
|
|
40
51
|
await guardProductionPush({
|
|
@@ -44,31 +55,61 @@ export class PushCommand extends LoopressCommand {
|
|
|
44
55
|
this.refusedByGuard = true;
|
|
45
56
|
},
|
|
46
57
|
siteConfig: this.siteConfig,
|
|
47
|
-
yes: this.yes,
|
|
58
|
+
yes: this.yes || this.productionConfirmed,
|
|
48
59
|
});
|
|
49
60
|
}
|
|
50
61
|
async init() {
|
|
51
62
|
await super.init();
|
|
52
63
|
await this.guardProductionPush();
|
|
53
64
|
}
|
|
65
|
+
// A dry run used to stay local, so it announced "pushed" for a resource the site would refuse
|
|
66
|
+
// (ACF or Rank Math missing: a 400 or 409 on the real push). It now reads the site the same way
|
|
67
|
+
// `lps diff` does and fails the same way, so the preview, and the MCP server's preview and
|
|
68
|
+
// pre-apply check built on it, see what the real push will hit.
|
|
69
|
+
async previewRemote(provider, dir) {
|
|
70
|
+
try {
|
|
71
|
+
await provider.remote(this.wp, () => { }, dir);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.error(`The site would refuse this push: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
54
77
|
async recordDeployment(status) {
|
|
55
78
|
const token = process.env.LOOPRESS_TOKEN ?? authManager.getAuth()?.token ?? null;
|
|
56
79
|
if (!token)
|
|
57
80
|
return;
|
|
58
81
|
try {
|
|
59
|
-
await got.post(`${API_URL}/deployments`, {
|
|
82
|
+
await withSpinner('Recording the deployment', async () => got.post(`${API_URL}/deployments`, {
|
|
60
83
|
headers: { Authorization: `Bearer ${token}` },
|
|
61
84
|
json: { status, url: this.siteConfig.url },
|
|
62
85
|
timeout: { request: 3000 },
|
|
63
|
-
});
|
|
86
|
+
}));
|
|
64
87
|
}
|
|
65
88
|
catch {
|
|
66
89
|
// non-blocking: recording must never interrupt the push flow
|
|
67
90
|
}
|
|
68
91
|
}
|
|
69
92
|
async recordSuccess() {
|
|
70
|
-
if (
|
|
71
|
-
|
|
93
|
+
if (this.dryRun)
|
|
94
|
+
return;
|
|
95
|
+
await this.recordDeployment('success');
|
|
96
|
+
await this.reportPushToSite();
|
|
97
|
+
}
|
|
98
|
+
// Tells the plugin this push finished, for its Overview tab and the "managed by Loopress"
|
|
99
|
+
// warnings in wp-admin: one record per command (`this.id`, e.g. `acf:push`), since a single push
|
|
100
|
+
// sends one request per item. Best-effort like recordDeployment: a plugin older than the push
|
|
101
|
+
// log answers 404, one that doesn't know this command 400, neither may fail a push that landed.
|
|
102
|
+
async reportPushToSite() {
|
|
103
|
+
if (!this.id)
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
// Bounded: the site fingerprints the pushed resource before answering, which a large
|
|
107
|
+
// site can make slow, and a finished push must not wait on its own bookkeeping.
|
|
108
|
+
await this.wp.post('loopress/v1/pushes', { resource: this.id }, { timeoutMs: 5000 });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// non-blocking, see above
|
|
112
|
+
}
|
|
72
113
|
}
|
|
73
114
|
// Reports one failed Listr task: status lines go through `task.output` when running inside a
|
|
74
115
|
// Listr renderer (this.warn would race with the repaint), and fall back to `this.warn` when
|
|
@@ -89,17 +130,19 @@ export class PushCommand extends LoopressCommand {
|
|
|
89
130
|
// just the first. `label` feeds the "Push <label>" task title; `task` does the actual work
|
|
90
131
|
// and reports its own failure through `reportTaskFailure`.
|
|
91
132
|
async runPushTasks(items, label, task) {
|
|
92
|
-
await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
133
|
+
await withoutSpinner(async () => {
|
|
134
|
+
await new Listr(items.map((item) => ({
|
|
135
|
+
task: async (_ctx, taskCtx) => task(item, taskCtx),
|
|
136
|
+
title: `Push ${label(item)}`,
|
|
137
|
+
})), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
138
|
+
});
|
|
96
139
|
}
|
|
97
140
|
// Pairs `beforeState` (from captureBeforePushState, read before this push touched anything)
|
|
98
141
|
// with a fresh remote read taken right after the push, and writes both as one rollback
|
|
99
142
|
// snapshot. Reading the after-state from WordPress itself, rather than assuming it matches
|
|
100
143
|
// the local files that were pushed, is what keeps this correct for every resource's own
|
|
101
|
-
// partial-push semantics: a readonly option skipped by `option push`, a snippet or
|
|
102
|
-
//
|
|
144
|
+
// partial-push semantics: a readonly option skipped by `option push`, a snippet or form
|
|
145
|
+
// that gets a server-assigned id on create, a theme-styles push that only touches the
|
|
103
146
|
// active stylesheet, an API/hook push that leaves server-side files alone unless `--prune`.
|
|
104
147
|
// `lps <resource> rollback`'s drift check compares the environment's state at rollback time
|
|
105
148
|
// against exactly this after-state, so it has to be what actually landed, not what was sent.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { isNotFoundError } from './wp-client.js';
|
|
2
|
-
// Update-or-create dance shared by every id-based resource (form,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// database reset.
|
|
2
|
+
// Update-or-create dance shared by every id-based resource (form, snippet, unlike ACF which
|
|
3
|
+
// is key-based and only ever POSTs): PUT to the known id, and only fall back to POST when the
|
|
4
|
+
// site doesn't recognize that id (404), e.g. a fresh install or a database reset.
|
|
6
5
|
export async function putOrCreate(wp, options) {
|
|
7
6
|
const { id, payload, postEndpoint, postPayload = payload, putEndpoint } = options;
|
|
8
7
|
if (id !== null) {
|