@loopress/cli 0.29.0 → 0.31.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 +1369 -360
- package/dist/commands/acf/push.js +2 -1
- package/dist/commands/composer/push.d.ts +1 -0
- package/dist/commands/composer/push.js +13 -0
- package/dist/commands/content/copy.d.ts +28 -0
- package/dist/commands/content/copy.js +97 -0
- 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 +2 -0
- package/dist/commands/doctor.js +57 -2
- package/dist/commands/form/push.js +4 -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/push.js +4 -2
- package/dist/commands/page/push.js +3 -2
- 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 +43 -25
- 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/promote.d.ts +1 -0
- package/dist/commands/promote.js +7 -3
- package/dist/commands/pull.d.ts +2 -1
- package/dist/commands/pull.js +31 -6
- package/dist/commands/push.d.ts +4 -2
- package/dist/commands/push.js +51 -19
- 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/push.d.ts +1 -0
- package/dist/commands/snippet/push.js +24 -3
- package/dist/commands/status.d.ts +2 -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/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +26 -5
- package/dist/commands/theme/version/pull.js +6 -1
- package/dist/config/project-config.manager.d.ts +3 -3
- package/dist/config/project-config.manager.js +22 -31
- package/dist/hooks/finally.js +11 -3
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/app-build.js +3 -1
- 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-command.d.ts +2 -0
- package/dist/lib/diff-command.js +24 -3
- package/dist/lib/github-release.js +4 -0
- package/dist/lib/load-snippets.js +4 -0
- package/dist/lib/local-callback-server.js +3 -1
- package/dist/lib/php-files-command.d.ts +8 -0
- package/dist/lib/php-files-command.js +5 -0
- package/dist/lib/push-command.d.ts +5 -2
- package/dist/lib/push-command.js +96 -15
- package/dist/lib/put-or-create.js +3 -4
- package/dist/lib/resource-state.js +38 -85
- package/dist/lib/rollback-command.js +9 -2
- package/dist/lib/rollback-materialize.js +13 -24
- 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 +40 -12
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +21 -2
- package/dist/lib/wp-site-diagnostic.js +3 -2
- package/dist/types/global-config.generated.d.ts +1 -5
- package/dist/types/project-config.generated.d.ts +28 -4
- package/dist/types/snippet.d.ts +1 -0
- package/dist/utils/composer.js +2 -1
- package/dist/utils/declared-type-format.d.ts +52 -0
- package/dist/utils/declared-type-format.js +233 -0
- package/dist/utils/resource-dirs.d.ts +9 -3
- package/dist/utils/resource-dirs.js +16 -4
- package/dist/utils/template-format.d.ts +1 -0
- package/dist/utils/theme-css.d.ts +19 -0
- package/dist/utils/theme-css.js +100 -0
- package/oclif.manifest.json +3294 -1213
- package/package.json +32 -16
- 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 -24
- package/dist/commands/seo/pull.js +0 -89
- package/dist/commands/seo/push.d.ts +0 -29
- package/dist/commands/seo/push.js +0 -178
- package/dist/commands/seo/rollback.js +0 -5
- 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,72 @@
|
|
|
1
|
+
// The one spinner behind every pending wait: WpClient and ApiClient requests, the browser
|
|
2
|
+
// callback, a build, a third-party lookup. Refcounted so overlapping waits share one line (the
|
|
3
|
+
// outermost label wins), on stderr and only on an interactive one, so `--json`, pipes and CI
|
|
4
|
+
// output never see a frame. While it spins, every stdout/stderr write clears the line first and
|
|
5
|
+
// redraws it after, so a this.log() mid-wait lands above it instead of on it.
|
|
6
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
let depth = 0;
|
|
8
|
+
let paused = 0;
|
|
9
|
+
let label = '';
|
|
10
|
+
let frame = 0;
|
|
11
|
+
let stopSpinning;
|
|
12
|
+
function isEnabled() {
|
|
13
|
+
return process.stderr.isTTY && !process.env.CI;
|
|
14
|
+
}
|
|
15
|
+
function start() {
|
|
16
|
+
const rawOut = process.stdout.write;
|
|
17
|
+
const rawErr = process.stderr.write;
|
|
18
|
+
const draw = () => {
|
|
19
|
+
const text = `${FRAMES[frame]} ${label}`.slice(0, (process.stderr.columns ?? 80) - 1);
|
|
20
|
+
rawErr.call(process.stderr, `\r\u{1B}[2K${text}`);
|
|
21
|
+
};
|
|
22
|
+
const clear = () => rawErr.call(process.stderr, '\r\u{1B}[2K');
|
|
23
|
+
const intercept = (stream, raw) => (...args) => {
|
|
24
|
+
clear();
|
|
25
|
+
const result = raw.apply(stream, args);
|
|
26
|
+
draw();
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
process.stdout.write = intercept(process.stdout, rawOut);
|
|
30
|
+
process.stderr.write = intercept(process.stderr, rawErr);
|
|
31
|
+
draw();
|
|
32
|
+
const timer = setInterval(() => {
|
|
33
|
+
frame = (frame + 1) % FRAMES.length;
|
|
34
|
+
draw();
|
|
35
|
+
}, 80);
|
|
36
|
+
timer.unref();
|
|
37
|
+
stopSpinning = () => {
|
|
38
|
+
clearInterval(timer);
|
|
39
|
+
process.stdout.write = rawOut;
|
|
40
|
+
process.stderr.write = rawErr;
|
|
41
|
+
clear();
|
|
42
|
+
stopSpinning = undefined;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function withSpinner(message, work) {
|
|
46
|
+
if (!isEnabled())
|
|
47
|
+
return work();
|
|
48
|
+
if (depth++ === 0)
|
|
49
|
+
label = message;
|
|
50
|
+
if (paused === 0 && !stopSpinning)
|
|
51
|
+
start();
|
|
52
|
+
try {
|
|
53
|
+
return await work();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
if (--depth === 0)
|
|
57
|
+
stopSpinning?.();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// For code that draws its own progress (Listr renderers, which already spin per task) or reads
|
|
61
|
+
// from the terminal (prompts): the spinner steps aside, then comes back if a wait is still pending.
|
|
62
|
+
export async function withoutSpinner(work) {
|
|
63
|
+
paused++;
|
|
64
|
+
stopSpinning?.();
|
|
65
|
+
try {
|
|
66
|
+
return await work();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (--paused === 0 && depth > 0 && isEnabled())
|
|
70
|
+
start();
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/lib/temp-admin.d.ts
CHANGED
|
@@ -16,4 +16,13 @@ export declare function createTempAdmin(wp: WpClient): Promise<TempAdmin>;
|
|
|
16
16
|
* cleanup that silently fails would leave a live administrator with a random password on the
|
|
17
17
|
* target site, discoverable only by someone reading logs closely.
|
|
18
18
|
*/
|
|
19
|
-
export declare function deleteTempAdmin(wp: WpClient, admin: TempAdmin): Promise<void>;
|
|
19
|
+
export declare function deleteTempAdmin(wp: WpClient, admin: Pick<TempAdmin, 'id' | 'username'>): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Lists temporary admins a previous bootstrap left behind: a process killed with SIGKILL, a
|
|
22
|
+
* machine that lost power, or a network drop during deleteTempAdmin() skips every cleanup path.
|
|
23
|
+
* Matching both the username prefix and the `.invalid` email keeps a real user who happens to
|
|
24
|
+
* be named `lps-temp-...` out of it. `context=edit` exposes `username` and `email` and lists
|
|
25
|
+
* every user, not only those with published posts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findTempAdmins(wp: WpClient): Promise<Array<Pick<TempAdmin, 'id' | 'username'>>>;
|
|
28
|
+
export declare function sweepTempAdmins(wp: WpClient): Promise<string[]>;
|
package/dist/lib/temp-admin.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { isNotFoundError } from './wp-client.js';
|
|
3
|
+
const TEMP_ADMIN_PREFIX = 'lps-temp-';
|
|
4
|
+
const TEMP_ADMIN_EMAIL_DOMAIN = '@lps-temp.invalid';
|
|
3
5
|
// Only role that carries `install_plugins`/`activate_plugins`; there's nothing narrower to grant.
|
|
4
6
|
const TEMP_ADMIN_ROLE = 'administrator';
|
|
5
7
|
/**
|
|
@@ -8,10 +10,10 @@ const TEMP_ADMIN_ROLE = 'administrator';
|
|
|
8
10
|
* addresses guaranteed not to be real, appropriate for an account that has no real mailbox.
|
|
9
11
|
*/
|
|
10
12
|
export async function createTempAdmin(wp) {
|
|
11
|
-
const username =
|
|
13
|
+
const username = `${TEMP_ADMIN_PREFIX}${Date.now().toString(36)}`;
|
|
12
14
|
const password = randomBytes(24).toString('base64url');
|
|
13
15
|
const user = await wp.post('wp/v2/users', {
|
|
14
|
-
email: `${username}
|
|
16
|
+
email: `${username}${TEMP_ADMIN_EMAIL_DOMAIN}`,
|
|
15
17
|
password,
|
|
16
18
|
roles: [TEMP_ADMIN_ROLE],
|
|
17
19
|
username,
|
|
@@ -44,3 +46,26 @@ export async function deleteTempAdmin(wp, admin) {
|
|
|
44
46
|
throw new Error(`Temporary admin account "${admin.username}" (id ${admin.id}) still exists after deletion. Remove it manually from wp-admin.`);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Lists temporary admins a previous bootstrap left behind: a process killed with SIGKILL, a
|
|
51
|
+
* machine that lost power, or a network drop during deleteTempAdmin() skips every cleanup path.
|
|
52
|
+
* Matching both the username prefix and the `.invalid` email keeps a real user who happens to
|
|
53
|
+
* be named `lps-temp-...` out of it. `context=edit` exposes `username` and `email` and lists
|
|
54
|
+
* every user, not only those with published posts.
|
|
55
|
+
*/
|
|
56
|
+
export async function findTempAdmins(wp) {
|
|
57
|
+
const users = await wp.getAll(`wp/v2/users?context=edit&search=${TEMP_ADMIN_PREFIX}`);
|
|
58
|
+
return users
|
|
59
|
+
.filter((user) => user.username.startsWith(TEMP_ADMIN_PREFIX) && user.email.endsWith(TEMP_ADMIN_EMAIL_DOMAIN))
|
|
60
|
+
.map(({ id, username }) => ({ id, username }));
|
|
61
|
+
}
|
|
62
|
+
// Deletes every leftover temp admin, returning the usernames removed. Throws on the first one
|
|
63
|
+
// that can't be removed, naming it (deleteTempAdmin's own error).
|
|
64
|
+
export async function sweepTempAdmins(wp) {
|
|
65
|
+
const leftovers = await findTempAdmins(wp);
|
|
66
|
+
for (const admin of leftovers) {
|
|
67
|
+
// Sequential on purpose: the first failure stops the sweep, naming its account.
|
|
68
|
+
await deleteTempAdmin(wp, admin);
|
|
69
|
+
}
|
|
70
|
+
return leftovers.map((admin) => admin.username);
|
|
71
|
+
}
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { configManager } from '../config/project-config.manager.js';
|
|
4
|
+
import { CPT_SPEC, declaredFileChecker, META_SPEC, PATTERN_SPEC, ROLE_SPEC, TAXONOMY_SPEC } from '../utils/declared-type-format.js';
|
|
4
5
|
import { parseLocalOption } from '../utils/option-format.js';
|
|
5
|
-
import { readLocalPages } from '../utils/page-format.js';
|
|
6
|
+
import { readLocalHtmlFiles, readLocalPages } from '../utils/page-format.js';
|
|
7
|
+
import { RESOURCE_DIR_DEFAULTS } from '../utils/resource-dirs.js';
|
|
6
8
|
import { parseType } from '../utils/snippet-format.js';
|
|
7
9
|
import { readLocalTemplates } from '../utils/template-format.js';
|
|
8
10
|
import { loadSnippets } from './load-snippets.js';
|
|
9
11
|
import { readdirTolerant } from './readdir-tolerant.js';
|
|
10
12
|
// Directories holding one JSON document per resource. Snippets (code file plus JSON sidecar)
|
|
11
13
|
// and api routes (`.php`, not JSON) are checked separately, options too (a stricter shape).
|
|
12
|
-
const JSON_RESOURCE_DIRS = ['acfDir', 'formDir', '
|
|
14
|
+
const JSON_RESOURCE_DIRS = ['acfDir', 'formDir', 'menuDir', 'themeStylesDir'];
|
|
13
15
|
const DEFAULT_DIR = {
|
|
14
16
|
acfDir: 'acf',
|
|
15
17
|
apiDir: 'api',
|
|
18
|
+
cptDir: 'cpt',
|
|
16
19
|
formDir: 'forms',
|
|
17
20
|
hooksDir: 'hooks',
|
|
18
21
|
menuDir: 'menus',
|
|
22
|
+
metaDir: 'meta',
|
|
19
23
|
optionsDir: 'options',
|
|
20
24
|
pageDir: 'pages',
|
|
21
|
-
partDir:
|
|
22
|
-
|
|
25
|
+
partDir: RESOURCE_DIR_DEFAULTS.part,
|
|
26
|
+
patternDir: RESOURCE_DIR_DEFAULTS.pattern,
|
|
27
|
+
roleDir: RESOURCE_DIR_DEFAULTS.role,
|
|
23
28
|
snippetsDir: 'snippets',
|
|
24
|
-
|
|
29
|
+
taxonomyDir: 'taxonomies',
|
|
30
|
+
templateDir: RESOURCE_DIR_DEFAULTS.template,
|
|
25
31
|
themeStylesDir: 'theme',
|
|
26
32
|
};
|
|
27
33
|
// Inspects the local tracked files under `cwd` without contacting WordPress: JSON files parse
|
|
@@ -46,6 +52,11 @@ export async function validateLocal(cwd) {
|
|
|
46
52
|
// Same reader `option push` uses: a file missing "name", "autoload" or "value" is skipped
|
|
47
53
|
// there with a warning, so it's a problem here.
|
|
48
54
|
checked += await checkJsonDir(resolve('optionsDir'), problems, parseLocalOption);
|
|
55
|
+
checked += await checkJsonDir(resolve('cptDir'), problems, declaredFileChecker(CPT_SPEC));
|
|
56
|
+
checked += await checkJsonDir(resolve('taxonomyDir'), problems, declaredFileChecker(TAXONOMY_SPEC));
|
|
57
|
+
checked += await checkJsonDir(resolve('metaDir'), problems, declaredFileChecker(META_SPEC));
|
|
58
|
+
checked += await checkJsonDir(resolve('roleDir'), problems, declaredFileChecker(ROLE_SPEC));
|
|
59
|
+
checked += await checkPatterns(resolve('patternDir'), problems);
|
|
49
60
|
checked += await checkSnippets(resolve('snippetsDir'), problems);
|
|
50
61
|
checked += await checkPhpDir(resolve('apiDir'), problems, 'API route file is empty');
|
|
51
62
|
checked += await checkPhpDir(resolve('hooksDir'), problems, 'Hook file is empty');
|
|
@@ -57,6 +68,18 @@ export async function validateLocal(cwd) {
|
|
|
57
68
|
problems.push(...templates.problems);
|
|
58
69
|
return { checked, problems, valid: problems.length === 0 };
|
|
59
70
|
}
|
|
71
|
+
// Same directory rules as pages (flat, .html only), then the same checks as `pattern push`.
|
|
72
|
+
async function checkPatterns(dir, problems) {
|
|
73
|
+
const check = declaredFileChecker(PATTERN_SPEC);
|
|
74
|
+
const { files, problems: found } = await readLocalHtmlFiles(dir, (slug, raw) => {
|
|
75
|
+
check(raw, join(dir, `${slug}.html`));
|
|
76
|
+
}, {
|
|
77
|
+
message: `the file name must be 1 to ${PATTERN_SPEC.slugMaxLength} lowercase letters, digits, "_" or "-"`,
|
|
78
|
+
pattern: new RegExp(`^[a-z0-9_-]{1,${PATTERN_SPEC.slugMaxLength}}$`),
|
|
79
|
+
});
|
|
80
|
+
problems.push(...found);
|
|
81
|
+
return files.length + found.length;
|
|
82
|
+
}
|
|
60
83
|
// Recursive: both api/ and hooks/ files can live in a nested path-segment directory
|
|
61
84
|
// (invoice-pdf/[order_id].php, content/filters.php), a non-recursive scan would silently
|
|
62
85
|
// never check those.
|
|
@@ -103,9 +126,8 @@ async function checkComposerJson(cwd, problems) {
|
|
|
103
126
|
}
|
|
104
127
|
return 1;
|
|
105
128
|
}
|
|
106
|
-
// Recursive: ACF keeps one subdirectory per object type (acf/field-groups/*.json)
|
|
107
|
-
//
|
|
108
|
-
// scan silently never checked any of those. `shape` runs on files that are JSON objects and
|
|
129
|
+
// Recursive: ACF keeps one subdirectory per object type (acf/field-groups/*.json); a
|
|
130
|
+
// top-level-only scan silently never checked those. `shape` runs on files that are JSON objects and
|
|
109
131
|
// throws to report a resource-specific problem.
|
|
110
132
|
async function checkJsonDir(dir, problems, shape) {
|
|
111
133
|
const entries = await readdirTolerant(dir, { recursive: true, withFileTypes: true });
|
|
@@ -130,7 +152,7 @@ async function checkJsonDir(dir, problems, shape) {
|
|
|
130
152
|
continue;
|
|
131
153
|
}
|
|
132
154
|
try {
|
|
133
|
-
shape?.(raw);
|
|
155
|
+
shape?.(raw, filePath);
|
|
134
156
|
}
|
|
135
157
|
catch (error) {
|
|
136
158
|
problems.push({ file: filePath, message: error.message });
|
|
@@ -156,12 +178,18 @@ function checkLoopressJson(localConfig, problems) {
|
|
|
156
178
|
if (plugins === undefined)
|
|
157
179
|
return;
|
|
158
180
|
if (typeof plugins !== 'object' || plugins === null || Array.isArray(plugins)) {
|
|
159
|
-
problems.push({ file: 'loopress.json', message: '"plugins" must be an object of slug -> version string' });
|
|
181
|
+
problems.push({ file: 'loopress.json', message: '"plugins" must be an object of slug -> version string or {version, active}' });
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
184
|
for (const [slug, version] of Object.entries(plugins)) {
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
// The object form is what `plugin pull` writes for a plugin installed but inactive (see PluginPin).
|
|
186
|
+
const isPin = typeof version === 'string' ||
|
|
187
|
+
(typeof version === 'object' &&
|
|
188
|
+
version !== null &&
|
|
189
|
+
typeof version.version === 'string' &&
|
|
190
|
+
['boolean', 'undefined'].includes(typeof version.active));
|
|
191
|
+
if (!isPin)
|
|
192
|
+
problems.push({ file: 'loopress.json', message: `plugin "${slug}" must map to a version string or {version, active}` });
|
|
165
193
|
}
|
|
166
194
|
}
|
|
167
195
|
async function checkSnippets(dir, problems) {
|
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ type RequestOptions = {
|
|
|
23
23
|
timeoutMs?: number;
|
|
24
24
|
};
|
|
25
25
|
export declare function isNotFoundError(error: unknown): boolean;
|
|
26
|
+
export declare function isForbiddenError(error: unknown): boolean;
|
|
26
27
|
export declare function isApplicative404(error: unknown, marker: string): boolean;
|
|
27
28
|
export declare function isTimeoutError(error: unknown): boolean;
|
|
28
29
|
export declare function isInvalidPageNumber(error: unknown): boolean;
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { withSpinner } from './spinner.js';
|
|
3
4
|
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
4
5
|
// The one environment allowed to talk plain HTTP: Loopress's local dev environment, identified
|
|
5
6
|
// by its config name (the same "local" environment `lps dev` targets), never by the URL's host.
|
|
@@ -81,7 +82,7 @@ export class WpClient {
|
|
|
81
82
|
async request(method, path, json, options) {
|
|
82
83
|
const timeoutMs = options?.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
83
84
|
try {
|
|
84
|
-
const response = await this.client(path, { json, method, timeout: { request: timeoutMs } });
|
|
85
|
+
const response = await withSpinner(`Waiting for ${new URL(this.siteUrl).host}`, async () => this.client(path, { json, method, timeout: { request: timeoutMs } }));
|
|
85
86
|
return (response.body ? JSON.parse(response.body) : undefined);
|
|
86
87
|
}
|
|
87
88
|
catch (error) {
|
|
@@ -93,6 +94,11 @@ export function isNotFoundError(error) {
|
|
|
93
94
|
const cause = error?.cause;
|
|
94
95
|
return cause?.response?.statusCode === 404;
|
|
95
96
|
}
|
|
97
|
+
// Authenticated but missing the capability the endpoint needs (e.g. `list_users`).
|
|
98
|
+
export function isForbiddenError(error) {
|
|
99
|
+
const cause = error?.cause;
|
|
100
|
+
return cause?.response?.statusCode === 403;
|
|
101
|
+
}
|
|
96
102
|
// A 404 a Loopress controller raises on purpose, identified by its JSON body's `error` field
|
|
97
103
|
// (e.g. `{"error": "composer.lock not found"}` from ComposerController). Narrower than
|
|
98
104
|
// isNotFoundError: a bare 404 also covers the route being absent because the plugin isn't
|
|
@@ -153,8 +159,21 @@ export function formatWpError(error, url, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
153
159
|
if (err.name === 'TimeoutError') {
|
|
154
160
|
return `Request timed out after ${timeoutMs / 1000}s on ${url}. Is the site reachable?`;
|
|
155
161
|
}
|
|
156
|
-
|
|
162
|
+
const reason = err.code ? NETWORK_ERROR_HINTS[err.code] : undefined;
|
|
163
|
+
// The site, not the endpoint: every resource fails the same way when the site is down, so
|
|
164
|
+
// `lps diff` can print it once (see DiffCommand.report).
|
|
165
|
+
if (reason)
|
|
166
|
+
return `Could not reach ${new URL(url).origin}: ${reason}. Run \`lps doctor\` to check the environment.`;
|
|
167
|
+
// Node leaves `message` empty on some network errors (an AggregateError over IPv4 + IPv6):
|
|
168
|
+
// fall back to the error code so the reason is never blank.
|
|
169
|
+
return `Request to ${url} failed: ${err.message?.length ? err.message : (err.code ?? String(error))}`;
|
|
157
170
|
}
|
|
171
|
+
const NETWORK_ERROR_HINTS = {
|
|
172
|
+
ECONNREFUSED: 'connection refused, the site may be down',
|
|
173
|
+
ECONNRESET: 'connection reset by the server',
|
|
174
|
+
ENOTFOUND: 'host not found, check the URL',
|
|
175
|
+
ETIMEDOUT: 'connection timed out',
|
|
176
|
+
};
|
|
158
177
|
function extractLoopressError(body) {
|
|
159
178
|
if (!body)
|
|
160
179
|
return undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
|
+
import { withSpinner } from './spinner.js';
|
|
2
3
|
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
4
|
/**
|
|
4
5
|
* WordPress 7.0 is the first version that accepts a loopback `http://127.0.0.1` success_url on
|
|
@@ -22,7 +23,7 @@ const MIN_BROWSER_AUTH_MAJOR = 7;
|
|
|
22
23
|
export async function diagnoseWpSite(siteUrl) {
|
|
23
24
|
let index;
|
|
24
25
|
try {
|
|
25
|
-
index = await got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } }).json();
|
|
26
|
+
index = await withSpinner(`Checking ${siteUrl}`, async () => got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } }).json());
|
|
26
27
|
}
|
|
27
28
|
catch (error) {
|
|
28
29
|
return {
|
|
@@ -57,7 +58,7 @@ async function detectWpVersion(siteUrl) {
|
|
|
57
58
|
];
|
|
58
59
|
for (const { pattern, url } of sources) {
|
|
59
60
|
try {
|
|
60
|
-
const body = await got.get(url, { timeout: { request: REQUEST_TIMEOUT_MS } }).text();
|
|
61
|
+
const body = await withSpinner(`Checking ${siteUrl}`, async () => got.get(url, { timeout: { request: REQUEST_TIMEOUT_MS } }).text());
|
|
61
62
|
const version = pattern.exec(body)?.[1];
|
|
62
63
|
if (version)
|
|
63
64
|
return version;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface LoopressGlobalConfiguration {
|
|
5
5
|
/**
|
|
6
|
-
* Pointer to the currently active project
|
|
6
|
+
* Pointer to the currently active project, or null if none is selected. The environment is not stored: commands use --env, else the project's "local" environment, else its only one.
|
|
7
7
|
*/
|
|
8
8
|
currentProject: null | CurrentProjectPointer;
|
|
9
9
|
/**
|
|
@@ -15,10 +15,6 @@ export interface LoopressGlobalConfiguration {
|
|
|
15
15
|
telemetry?: TelemetryConfig;
|
|
16
16
|
}
|
|
17
17
|
export interface CurrentProjectPointer {
|
|
18
|
-
/**
|
|
19
|
-
* Name of the currently active environment.
|
|
20
|
-
*/
|
|
21
|
-
env: string;
|
|
22
18
|
/**
|
|
23
19
|
* Id of the currently active project.
|
|
24
20
|
*/
|
|
@@ -15,10 +15,6 @@ export interface LoopressProjectConfiguration {
|
|
|
15
15
|
* Directory where ACF field groups, post types, taxonomies and options pages are read from and written to, relative to rootDir.
|
|
16
16
|
*/
|
|
17
17
|
acfDir?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Directory where SEO settings, post meta, and redirects (RankMath and Yoast SEO, whichever is active) are read from and written to, relative to rootDir.
|
|
20
|
-
*/
|
|
21
|
-
seoDir?: string;
|
|
22
18
|
/**
|
|
23
19
|
* Directory where forms (WPForms today) are read from and written to, relative to rootDir.
|
|
24
20
|
*/
|
|
@@ -47,10 +43,38 @@ export interface LoopressProjectConfiguration {
|
|
|
47
43
|
* Directory where block template parts (one <slug>.html per part, written to the Loopress child theme of the active block theme) are read from, relative to rootDir.
|
|
48
44
|
*/
|
|
49
45
|
partDir?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Directory of the theme's global CSS, relative to rootDir. Its style.css is the entry: it @imports the other files, bundled into the style.css of the Loopress child theme and loaded on every page.
|
|
48
|
+
*/
|
|
49
|
+
cssDir?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Directory of per-block CSS (one <namespace>/<block>.css per block, e.g. core/button.css), relative to rootDir. Each file is bundled into the Loopress child theme and only loaded on pages that render that block.
|
|
52
|
+
*/
|
|
53
|
+
blockCssDir?: string;
|
|
50
54
|
/**
|
|
51
55
|
* Directory where explicitly tracked WordPress options (wp_options rows) are read from and written to, relative to rootDir. Only options added via `lps option add` live here, this is never the whole options table.
|
|
52
56
|
*/
|
|
53
57
|
optionsDir?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Directory where custom post types (one JSON file of register_post_type() arguments per post type, named <slug>.json) are read from and written to, relative to rootDir.
|
|
60
|
+
*/
|
|
61
|
+
cptDir?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Directory where taxonomies (one JSON file of register_taxonomy() arguments plus "object_type" per taxonomy, named <slug>.json) are read from and written to, relative to rootDir.
|
|
64
|
+
*/
|
|
65
|
+
taxonomyDir?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Directory where user roles (one JSON file of "name" and "capabilities" per role, named <slug>.json) are read from and written to, relative to rootDir.
|
|
68
|
+
*/
|
|
69
|
+
roleDir?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Directory where block patterns (one <slug>.html of block markup per pattern, registered as loopress/<slug>) are read from and written to, relative to rootDir.
|
|
72
|
+
*/
|
|
73
|
+
patternDir?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Directory where post meta keys (one JSON file per post type, named <post-type>.json, mapping each meta key to its register_post_meta() arguments) are read from and written to, relative to rootDir.
|
|
76
|
+
*/
|
|
77
|
+
metaDir?: string;
|
|
54
78
|
/**
|
|
55
79
|
* Directory where nav menus (one JSON file per menu, plus menu-locations.json for the active theme's menu locations) are read from and written to, relative to rootDir.
|
|
56
80
|
*/
|
package/dist/types/snippet.d.ts
CHANGED
package/dist/utils/composer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import writeFileAtomic from 'write-file-atomic';
|
|
5
|
+
import { withSpinner } from '../lib/spinner.js';
|
|
5
6
|
import { isExactVersion } from './version.js';
|
|
6
7
|
// Why pullIntoComposerJson skips a plugin it can't install: premium or custom, not on WPackagist.
|
|
7
8
|
export const NOT_ON_WORDPRESS_ORG = 'not on WordPress.org';
|
|
@@ -44,7 +45,7 @@ async function isOnWordPressOrg(kind, slug) {
|
|
|
44
45
|
'Loopress only adds packages WPackagist can serve. Retry once api.wordpress.org is reachable.');
|
|
45
46
|
let response;
|
|
46
47
|
try {
|
|
47
|
-
response = await fetch(url);
|
|
48
|
+
response = await withSpinner(`Checking WordPress.org for ${slug}`, async () => fetch(url));
|
|
48
49
|
}
|
|
49
50
|
catch (error) {
|
|
50
51
|
throw fail(`network error: ${error.message}`);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ResourceDirKind } from './resource-dirs.js';
|
|
2
|
+
export type DeclaredTypeSpec = {
|
|
3
|
+
arrayArgs: Record<string, unknown[]>;
|
|
4
|
+
checkArgs?: (args: DeclaredArgs) => void;
|
|
5
|
+
cliName: 'cpt' | 'meta' | 'pattern' | 'role' | 'taxonomy';
|
|
6
|
+
codeArgs: string[];
|
|
7
|
+
dir: string;
|
|
8
|
+
dirKind: ResourceDirKind;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
file?: {
|
|
11
|
+
extension: string;
|
|
12
|
+
format(args: DeclaredArgs): string;
|
|
13
|
+
parse(slug: string, raw: string): DeclaredArgs;
|
|
14
|
+
};
|
|
15
|
+
noun: string;
|
|
16
|
+
plural: string;
|
|
17
|
+
registeredEndpoint: string;
|
|
18
|
+
registerFunction: string;
|
|
19
|
+
slugMaxLength: number;
|
|
20
|
+
termArgs: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare const CPT_SPEC: DeclaredTypeSpec;
|
|
23
|
+
export declare const TAXONOMY_SPEC: DeclaredTypeSpec;
|
|
24
|
+
export declare const META_SPEC: DeclaredTypeSpec;
|
|
25
|
+
export declare const ROLE_SPEC: DeclaredTypeSpec;
|
|
26
|
+
export declare function parsePatternFile(slug: string, raw: string): DeclaredArgs;
|
|
27
|
+
export declare function formatPatternFile(args: DeclaredArgs): string;
|
|
28
|
+
export declare const PATTERN_SPEC: DeclaredTypeSpec;
|
|
29
|
+
export declare function fileExtension(spec: DeclaredTypeSpec): string;
|
|
30
|
+
export declare function parseDeclaredFile(spec: DeclaredTypeSpec, filePath: string, raw: string): {
|
|
31
|
+
args: DeclaredArgs;
|
|
32
|
+
slug: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function formatDeclaredFile(spec: DeclaredTypeSpec, args: DeclaredArgs): string;
|
|
35
|
+
export declare function itemEndpoint(spec: DeclaredTypeSpec, slug: string): string;
|
|
36
|
+
export type DeclaredArgs = Record<string, unknown>;
|
|
37
|
+
export type RemoteDeclaredType = {
|
|
38
|
+
args: DeclaredArgs;
|
|
39
|
+
revision: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
};
|
|
42
|
+
export type RegisteredItem = {
|
|
43
|
+
conflict: boolean;
|
|
44
|
+
count: number;
|
|
45
|
+
label: string;
|
|
46
|
+
managed: boolean;
|
|
47
|
+
objectTypes?: string[];
|
|
48
|
+
slug: string;
|
|
49
|
+
source: 'acf' | 'cptui' | 'loopress' | 'other' | 'wordpress';
|
|
50
|
+
};
|
|
51
|
+
export declare function parseDeclaredArgs(raw: string): DeclaredArgs;
|
|
52
|
+
export declare function declaredFileChecker(spec: DeclaredTypeSpec): (raw: string, filePath: string) => void;
|