@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { LOCAL_ENVIRONMENT_NAME } from '../lib/wp-client.js';
|
|
3
4
|
import { toSlug } from '../utils/to-slug.js';
|
|
4
5
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
5
6
|
export class ProjectConfigManager {
|
|
@@ -39,15 +40,6 @@ export class ProjectConfigManager {
|
|
|
39
40
|
getConfigFilePath() {
|
|
40
41
|
return join(this.requireConfigDir(), 'config.json');
|
|
41
42
|
}
|
|
42
|
-
getCurrentEnv() {
|
|
43
|
-
const config = this.readConfig();
|
|
44
|
-
if (!config.currentProject)
|
|
45
|
-
return null;
|
|
46
|
-
const project = config.projects[config.currentProject.id];
|
|
47
|
-
if (!project)
|
|
48
|
-
return null;
|
|
49
|
-
return project.environments[config.currentProject.env] ?? null;
|
|
50
|
-
}
|
|
51
43
|
getCurrentProject() {
|
|
52
44
|
const config = this.readConfig();
|
|
53
45
|
if (!config.currentProject)
|
|
@@ -57,6 +49,17 @@ export class ProjectConfigManager {
|
|
|
57
49
|
return null;
|
|
58
50
|
return { ...project, id: config.currentProject.id };
|
|
59
51
|
}
|
|
52
|
+
// The environment a command targets without --env: "local" when the project has one (where
|
|
53
|
+
// you work, so it's the safe default), otherwise its only environment. Null when the project
|
|
54
|
+
// has several remote ones and no local: the user must say which one with --env, since a
|
|
55
|
+
// remembered choice is exactly the hidden state that sends a push to the wrong site.
|
|
56
|
+
getDefaultEnvironment(projectId) {
|
|
57
|
+
const environments = this.getProject(projectId)?.environments ?? {};
|
|
58
|
+
const names = Object.keys(environments);
|
|
59
|
+
if (Object.hasOwn(environments, LOCAL_ENVIRONMENT_NAME))
|
|
60
|
+
return environments[LOCAL_ENVIRONMENT_NAME];
|
|
61
|
+
return names.length === 1 ? environments[names[0]] : null;
|
|
62
|
+
}
|
|
60
63
|
getEnvironment(projectId, envName) {
|
|
61
64
|
const project = this.getProject(projectId);
|
|
62
65
|
if (!project)
|
|
@@ -71,14 +74,11 @@ export class ProjectConfigManager {
|
|
|
71
74
|
return this.readConfig().telemetry?.disabled ?? false;
|
|
72
75
|
}
|
|
73
76
|
listEnvironments(projectId) {
|
|
74
|
-
const
|
|
75
|
-
const project = config.projects[projectId];
|
|
77
|
+
const project = this.getProject(projectId);
|
|
76
78
|
if (!project)
|
|
77
79
|
return [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
isCurrent: config.currentProject?.id === projectId && config.currentProject.env === env.name,
|
|
81
|
-
}));
|
|
80
|
+
const defaultEnv = this.getDefaultEnvironment(projectId);
|
|
81
|
+
return Object.values(project.environments).map((env) => ({ ...env, isDefault: env.name === defaultEnv?.name }));
|
|
82
82
|
}
|
|
83
83
|
listProjects() {
|
|
84
84
|
const config = this.readConfig();
|
|
@@ -101,10 +101,6 @@ export class ProjectConfigManager {
|
|
|
101
101
|
if (!project)
|
|
102
102
|
return;
|
|
103
103
|
Reflect.deleteProperty(project.environments, envName);
|
|
104
|
-
if (config.currentProject?.id === projectId && config.currentProject.env === envName) {
|
|
105
|
-
const remaining = Object.keys(project.environments);
|
|
106
|
-
config.currentProject = remaining.length > 0 ? { env: remaining[0], id: projectId } : null;
|
|
107
|
-
}
|
|
108
104
|
this.writeConfig(config);
|
|
109
105
|
}
|
|
110
106
|
removeProject(id) {
|
|
@@ -112,9 +108,7 @@ export class ProjectConfigManager {
|
|
|
112
108
|
Reflect.deleteProperty(config.projects, id);
|
|
113
109
|
if (config.currentProject?.id === id) {
|
|
114
110
|
const [nextId] = Object.keys(config.projects);
|
|
115
|
-
|
|
116
|
-
const [nextEnv] = nextProject ? Object.keys(nextProject.environments) : [];
|
|
117
|
-
config.currentProject = nextId && nextEnv ? { env: nextEnv, id: nextId } : null;
|
|
111
|
+
config.currentProject = nextId ? { id: nextId } : null;
|
|
118
112
|
}
|
|
119
113
|
this.writeConfig(config);
|
|
120
114
|
}
|
|
@@ -124,11 +118,11 @@ export class ProjectConfigManager {
|
|
|
124
118
|
setConfigDir(configDir) {
|
|
125
119
|
this.configDir = configDir;
|
|
126
120
|
}
|
|
127
|
-
setCurrent(projectId
|
|
121
|
+
setCurrent(projectId) {
|
|
128
122
|
const config = this.readConfig();
|
|
129
123
|
if (!Object.hasOwn(config.projects, projectId))
|
|
130
124
|
return;
|
|
131
|
-
config.currentProject = {
|
|
125
|
+
config.currentProject = { id: projectId };
|
|
132
126
|
this.writeConfig(config);
|
|
133
127
|
}
|
|
134
128
|
setEnvironment(projectId, envName, env) {
|
|
@@ -137,7 +131,7 @@ export class ProjectConfigManager {
|
|
|
137
131
|
if (!project)
|
|
138
132
|
return;
|
|
139
133
|
project.environments[envName] = env;
|
|
140
|
-
config.currentProject ??= {
|
|
134
|
+
config.currentProject ??= { id: projectId };
|
|
141
135
|
this.writeConfig(config);
|
|
142
136
|
}
|
|
143
137
|
setEnvironmentApiId(projectId, envName, apiEnvironmentId) {
|
|
@@ -151,11 +145,7 @@ export class ProjectConfigManager {
|
|
|
151
145
|
setProject(id, project) {
|
|
152
146
|
const config = this.readConfig();
|
|
153
147
|
config.projects[id] = project;
|
|
154
|
-
|
|
155
|
-
const [firstEnv] = Object.keys(project.environments);
|
|
156
|
-
if (firstEnv)
|
|
157
|
-
config.currentProject = { env: firstEnv, id };
|
|
158
|
-
}
|
|
148
|
+
config.currentProject ??= { id };
|
|
159
149
|
this.writeConfig(config);
|
|
160
150
|
}
|
|
161
151
|
setProjectApiId(id, apiProjectId) {
|
|
@@ -203,7 +193,8 @@ export class ProjectConfigManager {
|
|
|
203
193
|
if (value === null || typeof value !== 'object')
|
|
204
194
|
return null;
|
|
205
195
|
const pointer = value;
|
|
206
|
-
|
|
196
|
+
// A legacy `env` field, from when switch also picked the environment, is dropped here.
|
|
197
|
+
return typeof pointer.id === 'string' ? { id: pointer.id } : null;
|
|
207
198
|
}
|
|
208
199
|
sanitizeProjects(value) {
|
|
209
200
|
if (typeof value !== 'object' || value === null)
|
package/dist/hooks/finally.js
CHANGED
|
@@ -27,10 +27,18 @@ async function report(context, options) {
|
|
|
27
27
|
dsn: SENTRY_DSN,
|
|
28
28
|
environment: resolveEnvironment(),
|
|
29
29
|
release: context.config.version,
|
|
30
|
+
// Sentry 11 collects all of these by default. Headers carry the app password, stack frame
|
|
31
|
+
// locals can hold it too, and this reports from users' own machines: collect none of it.
|
|
32
|
+
dataCollection: {
|
|
33
|
+
cookies: false,
|
|
34
|
+
httpBodies: [],
|
|
35
|
+
httpHeaders: false,
|
|
36
|
+
stackFrameVariables: false,
|
|
37
|
+
urlQueryParams: false,
|
|
38
|
+
userInfo: false,
|
|
39
|
+
},
|
|
30
40
|
// Node defaults `server_name` to os.hostname(), which is often the user's real name
|
|
31
|
-
// (e.g. "jane-doe-macbook-pro").
|
|
32
|
-
// default but set explicitly since this reports from users' own machines.
|
|
33
|
-
sendDefaultPii: false,
|
|
41
|
+
// (e.g. "jane-doe-macbook-pro").
|
|
34
42
|
serverName: 'loopress',
|
|
35
43
|
});
|
|
36
44
|
Sentry.captureException(options.error, {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
|
+
import { withSpinner } from './spinner.js';
|
|
2
3
|
import { REQUEST_TIMEOUT_MS } from './wp-client.js';
|
|
3
4
|
export const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
|
|
4
5
|
/**
|
|
@@ -29,7 +30,7 @@ export class ApiClient {
|
|
|
29
30
|
}
|
|
30
31
|
async request(method, path, json) {
|
|
31
32
|
try {
|
|
32
|
-
const response = await this.client(path, { json, method });
|
|
33
|
+
const response = await withSpinner('Waiting for the Loopress API', async () => this.client(path, { json, method }));
|
|
33
34
|
return (response.body ? JSON.parse(response.body) : undefined);
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
package/dist/lib/app-build.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { withSpinner } from './spinner.js';
|
|
5
6
|
// The package managers `lps app push` may spawn. An allowlist, not the raw `packageManager`
|
|
6
7
|
// field: the name ends up as a process to run.
|
|
7
8
|
const PACKAGE_MANAGERS = ['bun', 'npm', 'pnpm', 'yarn'];
|
|
@@ -43,7 +44,7 @@ export async function buildApp(appDir, onStart) {
|
|
|
43
44
|
const pm = detectPackageManager(appDir, pkg.packageManager);
|
|
44
45
|
const command = `${pm} run build`;
|
|
45
46
|
onStart?.(command);
|
|
46
|
-
const
|
|
47
|
+
const build = new Promise((resolve, reject) => {
|
|
47
48
|
// Windows resolves `npm` to `npm.cmd`, which only a shell finds. `pm` comes from the
|
|
48
49
|
// allowlist above, so the shell never sees user input.
|
|
49
50
|
const child = spawn(pm, ['run', 'build'], { cwd: appDir, shell: process.platform === 'win32' });
|
|
@@ -58,6 +59,7 @@ export async function buildApp(appDir, onStart) {
|
|
|
58
59
|
resolve({ code: exitCode, output: out });
|
|
59
60
|
});
|
|
60
61
|
});
|
|
62
|
+
const { code, output } = await withSpinner(`Running ${command}`, async () => build);
|
|
61
63
|
if (code !== 0) {
|
|
62
64
|
const tail = output.trimEnd().split('\n').slice(-OUTPUT_TAIL_LINES).join('\n');
|
|
63
65
|
const hint = existsSync(join(appDir, 'node_modules'))
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
14
14
|
};
|
|
15
15
|
protected dryRun: boolean;
|
|
16
16
|
protected localConfig: LoopressLocalConfig;
|
|
17
|
+
protected overwrite: boolean;
|
|
17
18
|
protected productionConfirmed: boolean;
|
|
18
19
|
protected projectId: string;
|
|
19
20
|
protected siteConfig: EnvironmentConfig;
|
|
@@ -39,10 +40,9 @@ export declare abstract class LoopressCommand extends Command {
|
|
|
39
40
|
protected resolveHooksPath(override?: string): string;
|
|
40
41
|
protected resolveMenuPath(override?: string): string;
|
|
41
42
|
protected resolveOptionsPath(override?: string): string;
|
|
42
|
-
protected resolveSeoPath(override?: string): string;
|
|
43
43
|
protected resolveSnippetsPath(override?: string): string;
|
|
44
44
|
protected resolveThemeStylesPath(override?: string): string;
|
|
45
|
+
protected warnRemovedSeoModule(): void;
|
|
45
46
|
private pickEnvironment;
|
|
46
47
|
private resolveEnvironment;
|
|
47
|
-
private resolveEnvironmentForConfiguredProject;
|
|
48
48
|
}
|
package/dist/lib/base.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import { Listr } from 'listr2';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import { mkdir, rm } from 'node:fs/promises';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { configManager } from '../config/project-config.manager.js';
|
|
@@ -9,13 +10,14 @@ import { pluralize } from '../utils/pluralize.js';
|
|
|
9
10
|
import { resolveResourceDir } from '../utils/resource-dirs.js';
|
|
10
11
|
import { isInteractive } from './interactive.js';
|
|
11
12
|
import { isAppPasswordStale, rotateAppPassword } from './rotate-app-password.js';
|
|
13
|
+
import { withoutSpinner } from './spinner.js';
|
|
12
14
|
import { WpClient } from './wp-client.js';
|
|
13
15
|
export class LoopressCommand extends Command {
|
|
14
|
-
// On every subclass without opt-in
|
|
15
|
-
//
|
|
16
|
+
// On every subclass without opt-in. Without it, commands target the "local" environment, so
|
|
17
|
+
// reaching a remote site is always spelled out in the command itself.
|
|
16
18
|
static baseFlags = {
|
|
17
19
|
env: Flags.string({
|
|
18
|
-
description: 'Target environment by name,
|
|
20
|
+
description: 'Target environment by name (default: "local", or the project\'s only environment)',
|
|
19
21
|
}),
|
|
20
22
|
};
|
|
21
23
|
static dryRunFlag = {
|
|
@@ -26,6 +28,8 @@ export class LoopressCommand extends Command {
|
|
|
26
28
|
};
|
|
27
29
|
dryRun = false;
|
|
28
30
|
localConfig = {};
|
|
31
|
+
// Set by PushCommand's --overwrite, see PushCommand.guardOutsideEdits.
|
|
32
|
+
overwrite = false;
|
|
29
33
|
// Set by PushCommand's hidden --production-confirmed, see there.
|
|
30
34
|
productionConfirmed = false;
|
|
31
35
|
projectId;
|
|
@@ -56,6 +60,7 @@ export class LoopressCommand extends Command {
|
|
|
56
60
|
}));
|
|
57
61
|
this.dryRun = Boolean(flags['dry-run']);
|
|
58
62
|
this.yes = Boolean(flags.yes);
|
|
63
|
+
this.overwrite = Boolean(flags.overwrite);
|
|
59
64
|
this.productionConfirmed = Boolean(flags['production-confirmed']);
|
|
60
65
|
this.localConfig = await readLocalConfig();
|
|
61
66
|
const resolved = this.resolveEnvironment(flags.env);
|
|
@@ -107,13 +112,15 @@ export class LoopressCommand extends Command {
|
|
|
107
112
|
}
|
|
108
113
|
if (options.alwaysCreateDir === true || items.length > 0)
|
|
109
114
|
await mkdir(dir, { recursive: true });
|
|
110
|
-
await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
await withoutSpinner(async () => {
|
|
116
|
+
await new Listr(items.map((item) => ({
|
|
117
|
+
async task(_ctx, task) {
|
|
118
|
+
await options.write(item, dir);
|
|
119
|
+
task.output = `Pulled: ${options.title(item)}`;
|
|
120
|
+
},
|
|
121
|
+
title: `Pull ${options.title(item)}`,
|
|
122
|
+
})), { renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
123
|
+
});
|
|
117
124
|
await this.removeOrphanedFiles(dir, orphans, options.orphanReason);
|
|
118
125
|
this.log(options.pulledMessage);
|
|
119
126
|
}
|
|
@@ -165,15 +172,19 @@ export class LoopressCommand extends Command {
|
|
|
165
172
|
resolveOptionsPath(override) {
|
|
166
173
|
return resolveResourceDir('options', this.localConfig, override);
|
|
167
174
|
}
|
|
168
|
-
resolveSeoPath(override) {
|
|
169
|
-
return resolveResourceDir('seo', this.localConfig, override);
|
|
170
|
-
}
|
|
171
175
|
resolveSnippetsPath(override) {
|
|
172
176
|
return resolveResourceDir('snippets', this.localConfig, override);
|
|
173
177
|
}
|
|
174
178
|
resolveThemeStylesPath(override) {
|
|
175
179
|
return resolveResourceDir('themeStyles', this.localConfig, override);
|
|
176
180
|
}
|
|
181
|
+
// `lps seo` was removed: a leftover seo/ folder or seoDir key would otherwise be silently
|
|
182
|
+
// ignored by `lps pull` / `lps push`. Remove once projects have had a release to migrate.
|
|
183
|
+
warnRemovedSeoModule() {
|
|
184
|
+
if (!('seoDir' in this.localConfig) && !existsSync(join(this.rootDir, 'seo')))
|
|
185
|
+
return;
|
|
186
|
+
this.warn('The SEO module was removed: seo/ is no longer pulled or pushed. Track SEO plugin settings with `lps option add rank-math-options-titles` (or `wpseo_titles`), then delete seo/ and any seoDir key from loopress.json.');
|
|
187
|
+
}
|
|
177
188
|
pickEnvironment(project, envName) {
|
|
178
189
|
const env = project.environments[envName];
|
|
179
190
|
if (!env) {
|
|
@@ -181,24 +192,12 @@ export class LoopressCommand extends Command {
|
|
|
181
192
|
}
|
|
182
193
|
return env;
|
|
183
194
|
}
|
|
195
|
+
// Order: --env, then the project's default (see getDefaultEnvironment). The project comes from
|
|
196
|
+
// loopress.json when pinned, from `lps project switch` otherwise.
|
|
184
197
|
resolveEnvironment(envName) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (envName) {
|
|
189
|
-
const current = configManager.getCurrentProject();
|
|
190
|
-
if (!current) {
|
|
191
|
-
this.error('No project configured. Run `lps project config` first.');
|
|
192
|
-
}
|
|
193
|
-
return { env: this.pickEnvironment(current, envName), projectId: current.id };
|
|
194
|
-
}
|
|
195
|
-
const env = configManager.getCurrentEnv();
|
|
196
|
-
const current = configManager.getCurrentProject();
|
|
197
|
-
if (env && current)
|
|
198
|
-
return { env, projectId: current.id };
|
|
199
|
-
this.error('No environment configured. Run `lps project config` first.');
|
|
200
|
-
}
|
|
201
|
-
resolveEnvironmentForConfiguredProject(projectId, envName) {
|
|
198
|
+
const projectId = this.localConfig.projectId ?? configManager.getCurrentProject()?.id;
|
|
199
|
+
if (!projectId)
|
|
200
|
+
this.error('No project configured. Run `lps project config` first.');
|
|
202
201
|
const project = configManager.getProject(projectId);
|
|
203
202
|
if (!project) {
|
|
204
203
|
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
@@ -209,14 +208,10 @@ export class LoopressCommand extends Command {
|
|
|
209
208
|
}
|
|
210
209
|
if (envName)
|
|
211
210
|
return { env: this.pickEnvironment(project, envName), projectId };
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const current = configManager.getCurrentProject();
|
|
216
|
-
const currentEnv = current?.id === projectId ? configManager.getCurrentEnv() : null;
|
|
217
|
-
if (!currentEnv) {
|
|
218
|
-
this.error(`Project "${project.name}" has multiple environments. Run \`lps project switch\` to pick one.`);
|
|
211
|
+
const env = configManager.getDefaultEnvironment(projectId);
|
|
212
|
+
if (!env) {
|
|
213
|
+
this.error(`Project "${project.name}" has no "local" environment, pass --env to pick one. Available: ${envNames.join(', ')}`);
|
|
219
214
|
}
|
|
220
|
-
return { env
|
|
215
|
+
return { env, projectId };
|
|
221
216
|
}
|
|
222
217
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type DeclaredTypeSpec, type RegisteredItem } from '../utils/declared-type-format.js';
|
|
2
|
+
import { LoopressCommand } from './base.js';
|
|
3
|
+
import { type CommandClass } from './php-files-command.js';
|
|
4
|
+
import { PushCommand } from './push-command.js';
|
|
5
|
+
type PushResult = {
|
|
6
|
+
path: string;
|
|
7
|
+
pushed: string[];
|
|
8
|
+
status: 'dry-run' | 'success';
|
|
9
|
+
};
|
|
10
|
+
type PullResult = {
|
|
11
|
+
path: string;
|
|
12
|
+
status: 'dry-run' | 'success';
|
|
13
|
+
};
|
|
14
|
+
type RmResult = {
|
|
15
|
+
removed: boolean;
|
|
16
|
+
slug: string;
|
|
17
|
+
status: 'aborted' | 'dry-run' | 'success';
|
|
18
|
+
};
|
|
19
|
+
export type DeclaredPushCommand = Omit<PushCommand, 'run'> & {
|
|
20
|
+
run(): Promise<PushResult>;
|
|
21
|
+
};
|
|
22
|
+
export declare function declaredPushCommand(spec: DeclaredTypeSpec, description: string): CommandClass<DeclaredPushCommand>;
|
|
23
|
+
export type DeclaredPullCommand = Omit<LoopressCommand, 'run'> & {
|
|
24
|
+
run(): Promise<PullResult>;
|
|
25
|
+
};
|
|
26
|
+
export declare function declaredPullCommand(spec: DeclaredTypeSpec, description: string): CommandClass<DeclaredPullCommand>;
|
|
27
|
+
export type DeclaredListCommand = Omit<LoopressCommand, 'run'> & {
|
|
28
|
+
run(): Promise<RegisteredItem[]>;
|
|
29
|
+
};
|
|
30
|
+
export declare function declaredListCommand(spec: DeclaredTypeSpec, description: string, countLabel: (count: number) => string): CommandClass<DeclaredListCommand>;
|
|
31
|
+
export type DeclaredRmCommand = Omit<LoopressCommand, 'run'> & {
|
|
32
|
+
run(): Promise<RmResult>;
|
|
33
|
+
};
|
|
34
|
+
export declare function declaredRmCommand(spec: DeclaredTypeSpec, description: string, keptNote: string): CommandClass<DeclaredRmCommand>;
|
|
35
|
+
export {};
|
|
@@ -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: 'theme:template:push', path: join(
|
|
22
|
-
templates: { commandId: 'theme: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
|
|
@@ -23,6 +23,7 @@ export type DiffTarget = {
|
|
|
23
23
|
type DiffSides = {
|
|
24
24
|
againstWp: undefined | WpClient;
|
|
25
25
|
leftLabel: string;
|
|
26
|
+
rightHeading: string;
|
|
26
27
|
rightLabel: string;
|
|
27
28
|
warn: (message: string) => void;
|
|
28
29
|
};
|
|
@@ -37,6 +38,7 @@ export declare abstract class DiffCommand extends LoopressCommand {
|
|
|
37
38
|
abstract run(): Promise<DiffJson>;
|
|
38
39
|
private computeResource;
|
|
39
40
|
private out;
|
|
41
|
+
private renderFailure;
|
|
40
42
|
private renderSection;
|
|
41
43
|
private wpForEnvironment;
|
|
42
44
|
}
|