@loopress/cli 0.32.1 → 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 +19 -7
- package/dist/lib/put-or-create.d.ts +2 -2
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.d.ts +1 -1
- package/dist/lib/resource-state.js +58 -19
- 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.d.ts +1 -0
- package/dist/utils/acf-format.js +11 -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 +44 -6
- package/dist/utils/version.js +1 -1
- package/oclif.manifest.json +1588 -1043
- package/package.json +42 -17
- package/schemas/global-config.schema.json +1 -1
- package/schemas/project-config.schema.json +11 -1
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { getAcfKey } from '../utils/acf-format.js';
|
|
5
|
+
import { writeBlock } from '../utils/block-format.js';
|
|
4
6
|
import { CPT_SPEC, fileExtension, formatDeclaredFile, META_SPEC, PATTERN_SPEC, ROLE_SPEC, TAXONOMY_SPEC } from '../utils/declared-type-format.js';
|
|
5
7
|
import { getFormId, getFormTitle } from '../utils/form-format.js';
|
|
6
8
|
import { getMenuSlug } from '../utils/menu-format.js';
|
|
7
9
|
import { defaultReadonlyFor, optionFileName } from '../utils/option-format.js';
|
|
8
|
-
import { buildMetaFile, buildSnippetFile } from '../utils/snippet-format.js';
|
|
10
|
+
import { buildMetaFile, buildSnippetFile, normalizeSnippet } from '../utils/snippet-format.js';
|
|
9
11
|
import { themeStylesFileName } from '../utils/theme-styles-format.js';
|
|
10
12
|
import { toSlug } from '../utils/to-slug.js';
|
|
13
|
+
import { isRecord } from './narrow.js';
|
|
11
14
|
const SNIPPET_EXTENSIONS = { css: 'css', html: 'html', js: 'js', php: 'php', text: 'txt' };
|
|
12
15
|
async function writeJson(path, value) {
|
|
13
16
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -19,9 +22,20 @@ async function writeJson(path, value) {
|
|
|
19
22
|
// compares (no `id`, no `description`, `code` with its `<?php` tag already stripped), exactly
|
|
20
23
|
// what `normalizeSnippet` + `stripPhpOpeningTag` produce, so it plugs straight into the same
|
|
21
24
|
// `buildSnippetFile`/`buildMetaFile` pair `snippet pull` uses to write files.
|
|
25
|
+
// A snapshot entry that should be an object: anything else is a corrupted snapshot file.
|
|
26
|
+
function snapshotRecord(id, value) {
|
|
27
|
+
if (!isRecord(value))
|
|
28
|
+
throw new Error(`Snapshot entry "${id}" is not an object.`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
const SNAPSHOT_OPTION = z.object({ autoload: z.string(), value: z.unknown() });
|
|
22
32
|
async function snippet(id, value, dir) {
|
|
23
|
-
const
|
|
24
|
-
|
|
33
|
+
const data = snapshotRecord(id, value);
|
|
34
|
+
// normalizeSnippet would coerce it, writing a corrupted entry's code over the live snippet.
|
|
35
|
+
if (typeof data.code !== 'string')
|
|
36
|
+
throw new Error(`Snapshot entry "${id}" has a non-string "code".`);
|
|
37
|
+
// normalizeSnippet is idempotent on its own output, so it narrows `data` without changing it.
|
|
38
|
+
const normalized = { ...normalizeSnippet(data), description: '', id: Number(id) };
|
|
25
39
|
const ext = SNIPPET_EXTENSIONS[normalized.type];
|
|
26
40
|
const base = `${normalized.id}-${toSlug(normalized.name)}`;
|
|
27
41
|
await mkdir(dir, { recursive: true });
|
|
@@ -31,7 +45,7 @@ async function snippet(id, value, dir) {
|
|
|
31
45
|
// value is the whole form JSON (minus `modified`/`modified_gmt`), the same shape `form pull`
|
|
32
46
|
// writes verbatim; `form push` re-sends it as-is.
|
|
33
47
|
async function form(id, value, dir) {
|
|
34
|
-
const data = value;
|
|
48
|
+
const data = snapshotRecord(id, value);
|
|
35
49
|
const formId = getFormId(data) ?? Number(id);
|
|
36
50
|
const slug = toSlug(getFormTitle(data), 'untitled');
|
|
37
51
|
await writeJson(join(dir, `${formId}-${slug}.json`), data);
|
|
@@ -41,7 +55,7 @@ async function form(id, value, dir) {
|
|
|
41
55
|
async function acf(id, value, dir) {
|
|
42
56
|
const separator = id.indexOf('/');
|
|
43
57
|
const type = id.slice(0, separator);
|
|
44
|
-
const data = value;
|
|
58
|
+
const data = snapshotRecord(id, value);
|
|
45
59
|
const key = getAcfKey(data) ?? id.slice(separator + 1);
|
|
46
60
|
await writeJson(join(dir, type, `${key}.json`), data);
|
|
47
61
|
}
|
|
@@ -51,13 +65,18 @@ async function phpFile(id, value, dir) {
|
|
|
51
65
|
await mkdir(dirname(filePath), { recursive: true });
|
|
52
66
|
await writeFile(filePath, String(value));
|
|
53
67
|
}
|
|
68
|
+
// block: id is the slug, value the folder's files (file name => text).
|
|
69
|
+
async function block(id, value, dir) {
|
|
70
|
+
const files = snapshotRecord(id, value);
|
|
71
|
+
await writeBlock(dir, { files: Object.fromEntries(Object.entries(files).map(([name, content]) => [name, String(content)])), slug: id });
|
|
72
|
+
}
|
|
54
73
|
// ids are 'menu-locations' or `menu/<slug>` (see resource-state.ts's menuProvider).
|
|
55
74
|
async function menu(id, value, dir) {
|
|
56
75
|
if (id === 'menu-locations') {
|
|
57
76
|
await writeJson(join(dir, 'menu-locations.json'), value);
|
|
58
77
|
return;
|
|
59
78
|
}
|
|
60
|
-
const data = value;
|
|
79
|
+
const data = snapshotRecord(id, value);
|
|
61
80
|
const slug = getMenuSlug(data) ?? id.slice('menu/'.length);
|
|
62
81
|
await writeJson(join(dir, `${slug}.json`), data);
|
|
63
82
|
}
|
|
@@ -67,7 +86,7 @@ async function menu(id, value, dir) {
|
|
|
67
86
|
// dangerous option (siteurl, home, ...); defaulting to the same policy `option add` itself uses
|
|
68
87
|
// keeps a rolled-back option exactly as protected as a freshly tracked one, never less.
|
|
69
88
|
async function option(id, value, dir) {
|
|
70
|
-
const { autoload, value: optionValue } = value;
|
|
89
|
+
const { autoload, value: optionValue } = SNAPSHOT_OPTION.parse(value);
|
|
71
90
|
const local = { autoload, name: id, readonly: defaultReadonlyFor(id), value: optionValue };
|
|
72
91
|
await writeJson(join(dir, optionFileName(id)), local);
|
|
73
92
|
}
|
|
@@ -79,12 +98,13 @@ async function themeStyles(id, value, dir) {
|
|
|
79
98
|
// written in the spec's file format like `<resource> pull` does.
|
|
80
99
|
function declaredType(spec) {
|
|
81
100
|
return async (id, value, dir) => {
|
|
82
|
-
await writeFile(join(dir, `${id}${fileExtension(spec)}`), formatDeclaredFile(spec, value));
|
|
101
|
+
await writeFile(join(dir, `${id}${fileExtension(spec)}`), formatDeclaredFile(spec, snapshotRecord(id, value)));
|
|
83
102
|
};
|
|
84
103
|
}
|
|
85
104
|
const MATERIALIZERS = {
|
|
86
105
|
acf,
|
|
87
106
|
api: phpFile,
|
|
107
|
+
block,
|
|
88
108
|
cpt: declaredType(CPT_SPEC),
|
|
89
109
|
form,
|
|
90
110
|
hook: phpFile,
|
package/dist/lib/sentry.js
CHANGED
|
@@ -20,8 +20,8 @@ export function runtimeContext() {
|
|
|
20
20
|
os: `${platform()} ${release()}`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
// Positional args and flag values
|
|
24
|
-
//
|
|
23
|
+
// Positional args and flag values are user input: environment names, local paths, slugs, and
|
|
24
|
+
// anything else typed after a command, possibly a URL or a credential. Only flag names are
|
|
25
25
|
// safe to report, they help tell which code path crashed without leaking what was passed to it.
|
|
26
26
|
export function redactArgv(argv) {
|
|
27
27
|
return argv.map((arg) => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { type ResourceState } from './diff-state.js';
|
|
2
3
|
export declare const SNAPSHOT_RETENTION = 10;
|
|
3
|
-
|
|
4
|
-
afterState:
|
|
5
|
-
beforeState:
|
|
6
|
-
createdAt:
|
|
7
|
-
environment:
|
|
8
|
-
resource:
|
|
9
|
-
}
|
|
4
|
+
declare const SNAPSHOT: z.ZodObject<{
|
|
5
|
+
afterState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
6
|
+
beforeState: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
7
|
+
createdAt: z.ZodString;
|
|
8
|
+
environment: z.ZodString;
|
|
9
|
+
resource: z.ZodString;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type Snapshot = z.infer<typeof SNAPSHOT>;
|
|
10
12
|
export type SnapshotSummary = {
|
|
11
13
|
createdAt: string;
|
|
12
14
|
environment: string;
|
|
@@ -28,3 +30,4 @@ export declare function readSnapshot(rootDir: string, resource: string, environm
|
|
|
28
30
|
id: string;
|
|
29
31
|
}>;
|
|
30
32
|
export declare function pruneSnapshots(rootDir: string, resource: string, environment: string, retain: number): Promise<void>;
|
|
33
|
+
export {};
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {} from './diff-state.js';
|
|
5
|
+
import { errorCode } from './narrow.js';
|
|
3
6
|
// How many snapshots `writeSnapshot` keeps per resource before pruning the oldest, so the
|
|
4
7
|
// cache directory doesn't grow without bound across a project's lifetime.
|
|
5
8
|
export const SNAPSHOT_RETENTION = 10;
|
|
9
|
+
// One `lps <resource> push` recorded as a restore point. `beforeState` is the remote state
|
|
10
|
+
// read immediately before that push (the "left" side `lps diff` would have shown); `afterState`
|
|
11
|
+
// is the local state that was about to be pushed (the "right" side). Rolling back restores
|
|
12
|
+
// `beforeState`; comparing the environment's *current* state against `afterState` at rollback
|
|
13
|
+
// time is what detects whether anything else touched the environment since.
|
|
14
|
+
const SNAPSHOT = z.object({
|
|
15
|
+
afterState: z.record(z.string(), z.unknown()),
|
|
16
|
+
beforeState: z.record(z.string(), z.unknown()),
|
|
17
|
+
createdAt: z.string(),
|
|
18
|
+
environment: z.string(),
|
|
19
|
+
resource: z.string(),
|
|
20
|
+
});
|
|
6
21
|
// `ResourceState` (a Map, for O(1) lookups during comparison) has no direct JSON
|
|
7
22
|
// representation; snapshots round-trip through a plain object instead. Keys are sorted so two
|
|
8
23
|
// snapshots of the same state serialize identically (stable diffs, deterministic tests).
|
|
@@ -27,10 +42,10 @@ function snapshotPath(rootDir, resource, environment, id) {
|
|
|
27
42
|
return join(snapshotsDir(rootDir, resource, environment), `${id}.json`);
|
|
28
43
|
}
|
|
29
44
|
function isEnoent(error) {
|
|
30
|
-
return error
|
|
45
|
+
return errorCode(error) === 'ENOENT';
|
|
31
46
|
}
|
|
32
47
|
function isEexist(error) {
|
|
33
|
-
return error
|
|
48
|
+
return errorCode(error) === 'EEXIST';
|
|
34
49
|
}
|
|
35
50
|
// Snapshot ids are the millisecond timestamp they were written at: sortable, unique enough for
|
|
36
51
|
// a single-machine CLI run, and directly usable as `--to <id>`.
|
|
@@ -105,7 +120,8 @@ export async function listSnapshots(rootDir, resource, environment) {
|
|
|
105
120
|
for (const entry of snapshotFiles) {
|
|
106
121
|
const id = entry.slice(0, -'.json'.length);
|
|
107
122
|
try {
|
|
108
|
-
const
|
|
123
|
+
const raw = JSON.parse(await readFile(join(dir, entry), 'utf8'));
|
|
124
|
+
const parsed = SNAPSHOT.parse(raw);
|
|
109
125
|
summaries.push({ createdAt: parsed.createdAt, environment: parsed.environment, id, resource });
|
|
110
126
|
}
|
|
111
127
|
catch {
|
|
@@ -122,7 +138,7 @@ export async function listSnapshots(rootDir, resource, environment) {
|
|
|
122
138
|
export async function readSnapshot(rootDir, resource, environment, id) {
|
|
123
139
|
let targetId = id;
|
|
124
140
|
if (targetId === undefined) {
|
|
125
|
-
const
|
|
141
|
+
const latest = (await listSnapshots(rootDir, resource, environment)).at(0);
|
|
126
142
|
if (!latest) {
|
|
127
143
|
throw new Error(`No snapshots found for "${resource}" on "${environment}". A snapshot is written automatically by \`lps ${resource} push\`.`);
|
|
128
144
|
}
|
|
@@ -138,7 +154,7 @@ export async function readSnapshot(rootDir, resource, environment, id) {
|
|
|
138
154
|
}
|
|
139
155
|
throw error;
|
|
140
156
|
}
|
|
141
|
-
return { ...JSON.parse(raw), id: targetId };
|
|
157
|
+
return { ...SNAPSHOT.parse(JSON.parse(raw)), id: targetId };
|
|
142
158
|
}
|
|
143
159
|
export async function pruneSnapshots(rootDir, resource, environment, retain) {
|
|
144
160
|
const summaries = await listSnapshots(rootDir, resource, environment);
|
package/dist/lib/spinner.js
CHANGED
|
@@ -16,6 +16,7 @@ function start() {
|
|
|
16
16
|
const rawOut = process.stdout.write;
|
|
17
17
|
const rawErr = process.stderr.write;
|
|
18
18
|
const draw = () => {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typed as number, but undefined when stderr is not a TTY
|
|
19
20
|
const text = `${FRAMES[frame]} ${label}`.slice(0, (process.stderr.columns ?? 80) - 1);
|
|
20
21
|
rawErr.call(process.stderr, `\r\u{1B}[2K${text}`);
|
|
21
22
|
};
|
|
@@ -26,7 +27,10 @@ function start() {
|
|
|
26
27
|
draw();
|
|
27
28
|
return result;
|
|
28
29
|
};
|
|
30
|
+
// write() is overloaded, and a single rest-args wrapper can't be typed as every overload.
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
29
32
|
process.stdout.write = intercept(process.stdout, rawOut);
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
30
34
|
process.stderr.write = intercept(process.stderr, rawErr);
|
|
31
35
|
draw();
|
|
32
36
|
const timer = setInterval(() => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { configManager } from '../config/project-config.manager.js';
|
|
5
|
+
import { PLUGIN_PIN } from '../types/plugin.js';
|
|
6
|
+
import { readLocalBlocks } from '../utils/block-format.js';
|
|
7
|
+
import { COMPOSER_JSON } from '../utils/composer.js';
|
|
4
8
|
import { CPT_SPEC, declaredFileChecker, META_SPEC, PATTERN_SPEC, ROLE_SPEC, TAXONOMY_SPEC } from '../utils/declared-type-format.js';
|
|
5
9
|
import { parseLocalOption } from '../utils/option-format.js';
|
|
6
10
|
import { readLocalHtmlFiles, readLocalPages } from '../utils/page-format.js';
|
|
@@ -8,6 +12,7 @@ import { RESOURCE_DIR_DEFAULTS } from '../utils/resource-dirs.js';
|
|
|
8
12
|
import { parseType } from '../utils/snippet-format.js';
|
|
9
13
|
import { readLocalTemplates } from '../utils/template-format.js';
|
|
10
14
|
import { loadSnippets } from './load-snippets.js';
|
|
15
|
+
import { errorCode, errorMessage, isRecord } from './narrow.js';
|
|
11
16
|
import { API_FILES_RESOURCE, HOOK_FILES_RESOURCE, phpFileProblems } from './php-files-command.js';
|
|
12
17
|
import { readdirTolerant } from './readdir-tolerant.js';
|
|
13
18
|
// Directories holding one JSON document per resource. Snippets (code file plus JSON sidecar)
|
|
@@ -16,6 +21,7 @@ const JSON_RESOURCE_DIRS = ['acfDir', 'formDir', 'menuDir', 'themeStylesDir'];
|
|
|
16
21
|
const DEFAULT_DIR = {
|
|
17
22
|
acfDir: 'acf',
|
|
18
23
|
apiDir: 'api',
|
|
24
|
+
blockDir: RESOURCE_DIR_DEFAULTS.block,
|
|
19
25
|
cptDir: 'cpt',
|
|
20
26
|
formDir: 'forms',
|
|
21
27
|
hooksDir: 'hooks',
|
|
@@ -61,6 +67,9 @@ export async function validateLocal(cwd) {
|
|
|
61
67
|
checked += await checkSnippets(resolve('snippetsDir'), problems);
|
|
62
68
|
checked += await checkPhpDir(resolve('apiDir'), problems, 'API route file is empty', API_FILES_RESOURCE);
|
|
63
69
|
checked += await checkPhpDir(resolve('hooksDir'), problems, 'Hook file is empty', HOOK_FILES_RESOURCE);
|
|
70
|
+
const blocks = await readLocalBlocks(resolve('blockDir'));
|
|
71
|
+
checked += blocks.blocks.length;
|
|
72
|
+
problems.push(...blocks.problems);
|
|
64
73
|
const pages = await readLocalPages(resolve('pageDir'));
|
|
65
74
|
checked += pages.pages.length + pages.problems.length;
|
|
66
75
|
problems.push(...pages.problems);
|
|
@@ -108,6 +117,7 @@ async function checkPhpDir(dir, problems, emptyMessage, spec) {
|
|
|
108
117
|
}
|
|
109
118
|
return checked;
|
|
110
119
|
}
|
|
120
|
+
const JSON_OBJECT = z.record(z.string(), z.unknown());
|
|
111
121
|
async function checkComposerJson(cwd, problems) {
|
|
112
122
|
let raw;
|
|
113
123
|
try {
|
|
@@ -122,14 +132,13 @@ async function checkComposerJson(cwd, problems) {
|
|
|
122
132
|
parsed = JSON.parse(raw);
|
|
123
133
|
}
|
|
124
134
|
catch (error) {
|
|
125
|
-
problems.push({ file: 'composer.json', message: `not valid JSON: ${error
|
|
135
|
+
problems.push({ file: 'composer.json', message: `not valid JSON: ${errorMessage(error)}` });
|
|
126
136
|
return 1;
|
|
127
137
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
problems.push({ file: 'composer.json', message
|
|
132
|
-
}
|
|
138
|
+
const result = COMPOSER_JSON.safeParse(parsed);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
for (const { message } of result.error.issues)
|
|
141
|
+
problems.push({ file: 'composer.json', message });
|
|
133
142
|
}
|
|
134
143
|
return 1;
|
|
135
144
|
}
|
|
@@ -151,10 +160,10 @@ async function checkJsonDir(dir, problems, shape) {
|
|
|
151
160
|
parsed = JSON.parse(raw);
|
|
152
161
|
}
|
|
153
162
|
catch (error) {
|
|
154
|
-
problems.push({ file: filePath, message: `not valid JSON: ${error
|
|
163
|
+
problems.push({ file: filePath, message: `not valid JSON: ${errorMessage(error)}` });
|
|
155
164
|
continue;
|
|
156
165
|
}
|
|
157
|
-
if (
|
|
166
|
+
if (!JSON_OBJECT.safeParse(parsed).success) {
|
|
158
167
|
problems.push({ file: filePath, message: 'expected a JSON object' });
|
|
159
168
|
continue;
|
|
160
169
|
}
|
|
@@ -162,7 +171,7 @@ async function checkJsonDir(dir, problems, shape) {
|
|
|
162
171
|
shape?.(raw, filePath);
|
|
163
172
|
}
|
|
164
173
|
catch (error) {
|
|
165
|
-
problems.push({ file: filePath, message: error
|
|
174
|
+
problems.push({ file: filePath, message: errorMessage(error) });
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
return checked;
|
|
@@ -184,19 +193,16 @@ function checkLoopressJson(localConfig, problems) {
|
|
|
184
193
|
const { plugins } = localConfig;
|
|
185
194
|
if (plugins === undefined)
|
|
186
195
|
return;
|
|
187
|
-
|
|
196
|
+
const manifest = JSON_OBJECT.safeParse(plugins);
|
|
197
|
+
if (!manifest.success) {
|
|
188
198
|
problems.push({ file: 'loopress.json', message: '"plugins" must be an object of slug -> version string or {version, active}' });
|
|
189
199
|
return;
|
|
190
200
|
}
|
|
191
|
-
for (const [slug, version] of Object.entries(
|
|
201
|
+
for (const [slug, version] of Object.entries(manifest.data)) {
|
|
192
202
|
// The object form is what `plugin pull` writes for a plugin installed but inactive (see PluginPin).
|
|
193
|
-
|
|
194
|
-
(typeof version === 'object' &&
|
|
195
|
-
version !== null &&
|
|
196
|
-
typeof version.version === 'string' &&
|
|
197
|
-
['boolean', 'undefined'].includes(typeof version.active));
|
|
198
|
-
if (!isPin)
|
|
203
|
+
if (!PLUGIN_PIN.safeParse(version).success) {
|
|
199
204
|
problems.push({ file: 'loopress.json', message: `plugin "${slug}" must map to a version string or {version, active}` });
|
|
205
|
+
}
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
async function checkSnippets(dir, problems) {
|
|
@@ -211,7 +217,7 @@ async function checkSnippets(dir, problems) {
|
|
|
211
217
|
});
|
|
212
218
|
}
|
|
213
219
|
catch (error) {
|
|
214
|
-
problems.push({ file: dir, message: error
|
|
220
|
+
problems.push({ file: dir, message: errorMessage(error) });
|
|
215
221
|
return 0;
|
|
216
222
|
}
|
|
217
223
|
// loadSnippets coerces an invalid `type` to the extension default rather than failing, so
|
|
@@ -227,6 +233,10 @@ async function checkSnippets(dir, problems) {
|
|
|
227
233
|
problems.push({ file: filePath, message: 'not valid JSON' });
|
|
228
234
|
continue;
|
|
229
235
|
}
|
|
236
|
+
if (!isRecord(meta)) {
|
|
237
|
+
problems.push({ file: filePath, message: 'expected a JSON object' });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
230
240
|
if (meta.type !== undefined && parseType(meta.type) === null) {
|
|
231
241
|
problems.push({ file: filePath, message: `"type": ${JSON.stringify(meta.type)} is not one of css, html, js, php, text` });
|
|
232
242
|
}
|
|
@@ -245,15 +255,20 @@ async function readConfig(cwd, problems) {
|
|
|
245
255
|
raw = await readFile(join(cwd, 'loopress.json'), 'utf8');
|
|
246
256
|
}
|
|
247
257
|
catch (error) {
|
|
248
|
-
if (error
|
|
258
|
+
if (errorCode(error) === 'ENOENT')
|
|
249
259
|
return {};
|
|
250
260
|
throw error;
|
|
251
261
|
}
|
|
262
|
+
let parsed;
|
|
252
263
|
try {
|
|
253
|
-
|
|
264
|
+
parsed = JSON.parse(raw);
|
|
254
265
|
}
|
|
255
266
|
catch {
|
|
256
267
|
problems.push({ file: 'loopress.json', message: 'loopress.json is not valid JSON. Fix or delete it, then run `lps init` again.' });
|
|
257
268
|
return {};
|
|
258
269
|
}
|
|
270
|
+
if (isRecord(parsed) && !Array.isArray(parsed))
|
|
271
|
+
return parsed;
|
|
272
|
+
problems.push({ file: 'loopress.json', message: 'expected a JSON object' });
|
|
273
|
+
return {};
|
|
259
274
|
}
|
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export declare function assertSecureSiteUrl(siteUrl: string, environmentName: st
|
|
|
7
7
|
* Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/snippets` or `wp/v2/plugins`.
|
|
8
8
|
*/
|
|
9
9
|
export declare class WpClient {
|
|
10
|
-
private readonly siteUrl;
|
|
11
10
|
private readonly client;
|
|
11
|
+
private readonly siteUrl;
|
|
12
12
|
constructor(siteUrl: string, token: string, environmentName?: string);
|
|
13
13
|
delete<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
14
14
|
get<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
@@ -28,5 +28,10 @@ export declare function isApplicative404(error: unknown, marker: string): boolea
|
|
|
28
28
|
export declare function isConflictCode(error: unknown, code: string): boolean;
|
|
29
29
|
export declare function isTimeoutError(error: unknown): boolean;
|
|
30
30
|
export declare function isInvalidPageNumber(error: unknown): boolean;
|
|
31
|
+
export declare function responseOf(error: unknown): {
|
|
32
|
+
body?: string | undefined;
|
|
33
|
+
statusCode?: number | undefined;
|
|
34
|
+
};
|
|
35
|
+
export declare function parseBody(body: string | undefined): Record<string, unknown>;
|
|
31
36
|
export declare function formatWpError(error: unknown, url: string, timeoutMs?: number): string;
|
|
32
37
|
export {};
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import got from 'got';
|
|
1
|
+
import got, {} from 'got';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { causeOf, errorCode, isRecord } from './narrow.js';
|
|
3
4
|
import { withSpinner } from './spinner.js';
|
|
4
5
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
6
|
// The one environment allowed to talk plain HTTP: Loopress's local dev environment, identified
|
|
@@ -30,11 +31,11 @@ export function assertSecureSiteUrl(siteUrl, environmentName) {
|
|
|
30
31
|
* Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/snippets` or `wp/v2/plugins`.
|
|
31
32
|
*/
|
|
32
33
|
export class WpClient {
|
|
33
|
-
siteUrl;
|
|
34
34
|
client;
|
|
35
|
+
siteUrl;
|
|
35
36
|
constructor(siteUrl, token, environmentName) {
|
|
36
|
-
this.siteUrl = siteUrl;
|
|
37
37
|
assertSecureSiteUrl(siteUrl, environmentName);
|
|
38
|
+
this.siteUrl = siteUrl;
|
|
38
39
|
this.client = got.extend({
|
|
39
40
|
headers: { Authorization: `Basic ${Buffer.from(token).toString('base64')}` },
|
|
40
41
|
prefixUrl: `${siteUrl}/wp-json`,
|
|
@@ -83,6 +84,8 @@ export class WpClient {
|
|
|
83
84
|
const timeoutMs = options?.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
84
85
|
try {
|
|
85
86
|
const response = await withSpinner(`Waiting for ${new URL(this.siteUrl).host}`, async () => this.client(path, { json, method, timeout: { request: timeoutMs } }));
|
|
87
|
+
// The REST contract boundary: each endpoint's shape is the caller's `T`, not checked here.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- see above
|
|
86
89
|
return (response.body ? JSON.parse(response.body) : undefined);
|
|
87
90
|
}
|
|
88
91
|
catch (error) {
|
|
@@ -91,95 +94,91 @@ export class WpClient {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
export function isNotFoundError(error) {
|
|
94
|
-
|
|
95
|
-
return cause?.response?.statusCode === 404;
|
|
97
|
+
return responseOf(causeOf(error)).statusCode === 404;
|
|
96
98
|
}
|
|
97
99
|
// Authenticated but missing the capability the endpoint needs (e.g. `list_users`).
|
|
98
100
|
export function isForbiddenError(error) {
|
|
99
|
-
|
|
100
|
-
return cause?.response?.statusCode === 403;
|
|
101
|
+
return responseOf(causeOf(error)).statusCode === 403;
|
|
101
102
|
}
|
|
102
103
|
// A 404 a Loopress controller raises on purpose, identified by its JSON body's `error` field
|
|
103
104
|
// (e.g. `{"error": "composer.lock not found"}` from ComposerController). Narrower than
|
|
104
105
|
// isNotFoundError: a bare 404 also covers the route being absent because the plugin isn't
|
|
105
106
|
// installed or predates the feature, which callers must still surface normally.
|
|
106
107
|
export function isApplicative404(error, marker) {
|
|
107
|
-
|
|
108
|
-
return false;
|
|
109
|
-
const body = error.cause?.response?.body;
|
|
110
|
-
if (!body)
|
|
111
|
-
return false;
|
|
112
|
-
try {
|
|
113
|
-
return JSON.parse(body).error === marker;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
108
|
+
return isNotFoundError(error) && parseBody(responseOf(causeOf(error)).body).error === marker;
|
|
118
109
|
}
|
|
119
110
|
// A 409 a Loopress controller tags with a `code`, for a refusal the CLI can act on (e.g.
|
|
120
111
|
// `loopress_page_not_managed`, which `lps page push` offers to take over).
|
|
121
112
|
export function isConflictCode(error, code) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
return false;
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(response.body).code === code;
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
113
|
+
const { body, statusCode } = responseOf(causeOf(error));
|
|
114
|
+
return statusCode === 409 && parseBody(body).code === code;
|
|
131
115
|
}
|
|
132
116
|
export function isTimeoutError(error) {
|
|
133
|
-
const cause = error
|
|
134
|
-
return cause
|
|
117
|
+
const cause = causeOf(error);
|
|
118
|
+
return isRecord(cause) && cause.name === 'TimeoutError';
|
|
135
119
|
}
|
|
136
120
|
// WordPress core answers 400 with this `code` when `?page=N` is past the last page of a
|
|
137
121
|
// collection, which is how `WpClient.getAll` knows it has walked them all.
|
|
138
122
|
export function isInvalidPageNumber(error) {
|
|
139
|
-
|
|
123
|
+
return parseBody(responseOf(causeOf(error)).body).code === 'rest_post_invalid_page_number';
|
|
124
|
+
}
|
|
125
|
+
// The HTTP response a got error carries, read structurally so a mocked error reads the same.
|
|
126
|
+
export function responseOf(error) {
|
|
127
|
+
const response = isRecord(error) ? error.response : undefined;
|
|
128
|
+
if (!isRecord(response))
|
|
129
|
+
return {};
|
|
130
|
+
return {
|
|
131
|
+
body: typeof response.body === 'string' ? response.body : undefined,
|
|
132
|
+
statusCode: typeof response.statusCode === 'number' ? response.statusCode : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// A response body's top-level JSON fields, or none when it isn't JSON.
|
|
136
|
+
export function parseBody(body) {
|
|
140
137
|
if (!body)
|
|
141
|
-
return
|
|
138
|
+
return {};
|
|
142
139
|
try {
|
|
143
|
-
|
|
140
|
+
const parsed = JSON.parse(body);
|
|
141
|
+
return isRecord(parsed) ? parsed : {};
|
|
144
142
|
}
|
|
145
143
|
catch {
|
|
146
|
-
return
|
|
144
|
+
return {};
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
147
|
export function formatWpError(error, url, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
150
|
-
const
|
|
151
|
-
const status = err.response?.statusCode;
|
|
148
|
+
const { body, statusCode: status } = responseOf(error);
|
|
152
149
|
if (status === 401 || status === 403) {
|
|
153
150
|
// A Loopress controller's own `{"error": ...}` on a 403 is a deliberate refusal of this one
|
|
154
151
|
// request (e.g. OptionsController protecting a secret-looking option), not bad credentials:
|
|
155
152
|
// pointing at `lps project config` there sends the user the wrong way. WordPress core's own
|
|
156
153
|
// auth failures use `{code, message}` instead and keep the credentials hint.
|
|
157
|
-
const refusal = extractLoopressError(
|
|
154
|
+
const refusal = extractLoopressError(body);
|
|
158
155
|
if (refusal)
|
|
159
156
|
return `Request refused (${status}) on ${url}: ${refusal}`;
|
|
160
157
|
return `Authentication failed (${status}) on ${url}. Check your credentials with \`lps project config\`.`;
|
|
161
158
|
}
|
|
162
159
|
if (status === 404) {
|
|
163
|
-
const reason = extractServerErrorMessage(
|
|
160
|
+
const reason = extractServerErrorMessage(body);
|
|
164
161
|
return reason
|
|
165
162
|
? `Request failed (404) on ${url}: ${reason}`
|
|
166
163
|
: `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
|
|
167
164
|
}
|
|
168
165
|
if (status !== undefined) {
|
|
169
|
-
const reason = extractServerErrorMessage(
|
|
166
|
+
const reason = extractServerErrorMessage(body);
|
|
170
167
|
return reason ? `Request failed (${status}) on ${url}: ${reason}` : `Request failed (${status}) on ${url}.`;
|
|
171
168
|
}
|
|
172
|
-
if (
|
|
169
|
+
if (isRecord(error) && error.name === 'TimeoutError') {
|
|
173
170
|
return `Request timed out after ${timeoutMs / 1000}s on ${url}. Is the site reachable?`;
|
|
174
171
|
}
|
|
175
|
-
const
|
|
172
|
+
const code = errorCode(error);
|
|
173
|
+
const reason = code ? NETWORK_ERROR_HINTS[code] : undefined;
|
|
176
174
|
// The site, not the endpoint: every resource fails the same way when the site is down, so
|
|
177
175
|
// `lps diff` can print it once (see DiffCommand.report).
|
|
178
176
|
if (reason)
|
|
179
177
|
return `Could not reach ${new URL(url).origin}: ${reason}. Run \`lps doctor\` to check the environment.`;
|
|
180
178
|
// Node leaves `message` empty on some network errors (an AggregateError over IPv4 + IPv6):
|
|
181
179
|
// fall back to the error code so the reason is never blank.
|
|
182
|
-
|
|
180
|
+
const message = isRecord(error) && typeof error.message === 'string' ? error.message : '';
|
|
181
|
+
return `Request to ${url} failed: ${message || (code ?? String(error))}`;
|
|
183
182
|
}
|
|
184
183
|
const NETWORK_ERROR_HINTS = {
|
|
185
184
|
ECONNREFUSED: 'connection refused, the site may be down',
|
|
@@ -188,15 +187,8 @@ const NETWORK_ERROR_HINTS = {
|
|
|
188
187
|
ETIMEDOUT: 'connection timed out',
|
|
189
188
|
};
|
|
190
189
|
function extractLoopressError(body) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const { error } = JSON.parse(body);
|
|
195
|
-
return typeof error === 'string' && error.trim() ? decodeEntities(error) : undefined;
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
190
|
+
const { error } = parseBody(body);
|
|
191
|
+
return typeof error === 'string' && error.trim() ? decodeEntities(error) : undefined;
|
|
200
192
|
}
|
|
201
193
|
// Plugin exception messages go through esc_html() (WordPress coding standards require it for
|
|
202
194
|
// anything thrown), so quotes and ampersands reach the JSON body as entities. They're meant for
|
|
@@ -219,24 +211,17 @@ function decodeEntities(text) {
|
|
|
219
211
|
// specifically so a caller that only reads `error` doesn't see it, which is exactly what this
|
|
220
212
|
// function used to do, hiding the one piece of text that actually explains the failure.
|
|
221
213
|
function extractServerErrorMessage(body) {
|
|
222
|
-
|
|
214
|
+
const parsed = parseBody(body);
|
|
215
|
+
// WP core's own generic body for a route it never registered (e.g. a plugin too old to
|
|
216
|
+
// expose this endpoint yet) is never more useful than the "is the plugin up to date?"
|
|
217
|
+
// fallback below; surfacing it verbatim buries a version-skew mismatch behind text that
|
|
218
|
+
// reads like the endpoint doesn't exist at all, pointing the user in the wrong direction.
|
|
219
|
+
if (parsed.code === 'rest_no_route')
|
|
223
220
|
return undefined;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (parsed.code === 'rest_no_route')
|
|
231
|
-
return undefined;
|
|
232
|
-
const reason = parsed.error ?? parsed.message;
|
|
233
|
-
const summary = typeof reason === 'string' && reason.trim() ? decodeEntities(reason) : undefined;
|
|
234
|
-
const detail = typeof parsed.output === 'string' && parsed.output.trim() ? parsed.output.trim() : undefined;
|
|
235
|
-
if (summary && detail)
|
|
236
|
-
return `${summary}\n${detail}`;
|
|
237
|
-
return summary ?? detail;
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
221
|
+
const reason = parsed.error ?? parsed.message;
|
|
222
|
+
const summary = typeof reason === 'string' && reason.trim() ? decodeEntities(reason) : undefined;
|
|
223
|
+
const detail = typeof parsed.output === 'string' && parsed.output.trim() ? parsed.output.trim() : undefined;
|
|
224
|
+
if (summary && detail)
|
|
225
|
+
return `${summary}\n${detail}`;
|
|
226
|
+
return summary ?? detail;
|
|
242
227
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
|
+
import { errorMessage, isRecord } from './narrow.js';
|
|
2
3
|
import { withSpinner } from './spinner.js';
|
|
3
4
|
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
4
5
|
/**
|
|
@@ -28,10 +29,10 @@ export async function diagnoseWpSite(siteUrl) {
|
|
|
28
29
|
catch (error) {
|
|
29
30
|
return {
|
|
30
31
|
ok: false,
|
|
31
|
-
reason: `Could not reach the WordPress REST API at ${siteUrl}/wp-json/. The site may be unreachable, or a security plugin may be blocking it. (${
|
|
32
|
+
reason: `Could not reach the WordPress REST API at ${siteUrl}/wp-json/. The site may be unreachable, or a security plugin may be blocking it. (${errorMessage(error)})`,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
|
-
if (!index?.authentication?.['application-passwords']) {
|
|
35
|
+
if (!isRecord(index?.authentication?.['application-passwords'])) {
|
|
35
36
|
return {
|
|
36
37
|
ok: false,
|
|
37
38
|
reason: `Application Passwords are not available on ${siteUrl}. The site may be older than WordPress 5.6, require HTTPS, or have the feature disabled by a plugin or filter.`,
|
|
@@ -69,6 +70,3 @@ async function detectWpVersion(siteUrl) {
|
|
|
69
70
|
}
|
|
70
71
|
return undefined;
|
|
71
72
|
}
|
|
72
|
-
function describe(error) {
|
|
73
|
-
return error instanceof Error ? error.message : String(error);
|
|
74
|
-
}
|