@loopress/cli 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +1460 -337
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/acf/pull.d.ts +9 -1
  4. package/dist/commands/acf/pull.js +2 -0
  5. package/dist/commands/acf/push.d.ts +9 -1
  6. package/dist/commands/acf/push.js +3 -1
  7. package/dist/commands/api/publish.js +1 -1
  8. package/dist/commands/app/push.js +1 -1
  9. package/dist/commands/composer/push.js +3 -3
  10. package/dist/commands/cpt/diff.js +5 -0
  11. package/dist/commands/cpt/list.d.ts +2 -0
  12. package/dist/commands/cpt/list.js +4 -0
  13. package/dist/commands/cpt/pull.d.ts +2 -0
  14. package/dist/commands/cpt/pull.js +4 -0
  15. package/dist/commands/cpt/push.d.ts +2 -0
  16. package/dist/commands/cpt/push.js +4 -0
  17. package/dist/commands/cpt/rm.d.ts +2 -0
  18. package/dist/commands/cpt/rm.js +3 -0
  19. package/dist/commands/cpt/rollback.js +5 -0
  20. package/dist/commands/dev.js +14 -4
  21. package/dist/commands/diff.js +1 -1
  22. package/dist/commands/doctor.d.ts +1 -0
  23. package/dist/commands/doctor.js +30 -2
  24. package/dist/commands/form/pull.d.ts +9 -1
  25. package/dist/commands/form/pull.js +3 -1
  26. package/dist/commands/form/push.d.ts +7 -1
  27. package/dist/commands/form/push.js +3 -1
  28. package/dist/commands/init.js +6 -38
  29. package/dist/commands/menu/pull.d.ts +7 -1
  30. package/dist/commands/menu/pull.js +2 -0
  31. package/dist/commands/menu/push.d.ts +7 -1
  32. package/dist/commands/menu/push.js +3 -1
  33. package/dist/commands/meta/diff.d.ts +2 -0
  34. package/dist/commands/meta/diff.js +5 -0
  35. package/dist/commands/meta/list.d.ts +19 -0
  36. package/dist/commands/meta/list.js +21 -0
  37. package/dist/commands/meta/pull.d.ts +2 -0
  38. package/dist/commands/meta/pull.js +4 -0
  39. package/dist/commands/meta/push.d.ts +2 -0
  40. package/dist/commands/meta/push.js +4 -0
  41. package/dist/commands/meta/rm.d.ts +2 -0
  42. package/dist/commands/meta/rm.js +3 -0
  43. package/dist/commands/meta/rollback.d.ts +2 -0
  44. package/dist/commands/meta/rollback.js +5 -0
  45. package/dist/commands/option/list.js +2 -8
  46. package/dist/commands/option/pull.js +4 -4
  47. package/dist/commands/option/push.js +2 -0
  48. package/dist/commands/page/push.js +3 -0
  49. package/dist/commands/pattern/diff.d.ts +2 -0
  50. package/dist/commands/pattern/diff.js +5 -0
  51. package/dist/commands/pattern/list.d.ts +2 -0
  52. package/dist/commands/pattern/list.js +3 -0
  53. package/dist/commands/pattern/pull.d.ts +2 -0
  54. package/dist/commands/pattern/pull.js +4 -0
  55. package/dist/commands/pattern/push.d.ts +2 -0
  56. package/dist/commands/pattern/push.js +4 -0
  57. package/dist/commands/pattern/rm.d.ts +2 -0
  58. package/dist/commands/pattern/rm.js +3 -0
  59. package/dist/commands/pattern/rollback.d.ts +2 -0
  60. package/dist/commands/pattern/rollback.js +5 -0
  61. package/dist/commands/plugin/audit.js +2 -1
  62. package/dist/commands/plugin/pull.d.ts +4 -0
  63. package/dist/commands/plugin/pull.js +56 -10
  64. package/dist/commands/plugin/push.js +5 -4
  65. package/dist/commands/project/config.d.ts +1 -0
  66. package/dist/commands/project/config.js +20 -1
  67. package/dist/commands/project/list.d.ts +1 -1
  68. package/dist/commands/project/list.js +6 -6
  69. package/dist/commands/project/pull.js +11 -8
  70. package/dist/commands/project/push.js +23 -18
  71. package/dist/commands/project/remove.js +1 -1
  72. package/dist/commands/project/switch.d.ts +0 -1
  73. package/dist/commands/project/switch.js +16 -36
  74. package/dist/commands/pull.js +7 -2
  75. package/dist/commands/push.js +14 -6
  76. package/dist/commands/role/diff.d.ts +2 -0
  77. package/dist/commands/role/diff.js +5 -0
  78. package/dist/commands/role/list.d.ts +2 -0
  79. package/dist/commands/role/list.js +4 -0
  80. package/dist/commands/role/pull.d.ts +2 -0
  81. package/dist/commands/role/pull.js +4 -0
  82. package/dist/commands/role/push.d.ts +2 -0
  83. package/dist/commands/role/push.js +4 -0
  84. package/dist/commands/role/rm.d.ts +2 -0
  85. package/dist/commands/role/rm.js +3 -0
  86. package/dist/commands/role/rollback.d.ts +2 -0
  87. package/dist/commands/role/rollback.js +5 -0
  88. package/dist/commands/snippet/publish.js +1 -1
  89. package/dist/commands/snippet/pull.js +1 -1
  90. package/dist/commands/snippet/push.js +1 -1
  91. package/dist/commands/status.d.ts +3 -3
  92. package/dist/commands/status.js +21 -53
  93. package/dist/commands/taxonomy/diff.d.ts +2 -0
  94. package/dist/commands/taxonomy/diff.js +5 -0
  95. package/dist/commands/taxonomy/list.d.ts +2 -0
  96. package/dist/commands/taxonomy/list.js +5 -0
  97. package/dist/commands/taxonomy/pull.d.ts +2 -0
  98. package/dist/commands/taxonomy/pull.js +4 -0
  99. package/dist/commands/taxonomy/push.d.ts +2 -0
  100. package/dist/commands/taxonomy/push.js +4 -0
  101. package/dist/commands/taxonomy/rm.d.ts +2 -0
  102. package/dist/commands/taxonomy/rm.js +3 -0
  103. package/dist/commands/taxonomy/rollback.d.ts +2 -0
  104. package/dist/commands/taxonomy/rollback.js +5 -0
  105. package/dist/commands/theme/pull.d.ts +6 -10
  106. package/dist/commands/theme/pull.js +30 -32
  107. package/dist/commands/theme/push.d.ts +8 -9
  108. package/dist/commands/theme/push.js +54 -111
  109. package/dist/commands/theme/style/pull.d.ts +7 -1
  110. package/dist/commands/theme/style/pull.js +3 -1
  111. package/dist/commands/theme/style/push.d.ts +8 -1
  112. package/dist/commands/theme/style/push.js +4 -1
  113. package/dist/commands/{template → theme/template}/diff.d.ts +1 -1
  114. package/dist/commands/{template → theme/template}/diff.js +3 -3
  115. package/dist/commands/{template → theme/template}/list.d.ts +2 -2
  116. package/dist/commands/{template → theme/template}/list.js +5 -5
  117. package/dist/commands/{template → theme/template}/push.d.ts +3 -1
  118. package/dist/commands/theme/template/push.js +65 -0
  119. package/dist/commands/theme/version/pull.d.ts +23 -0
  120. package/dist/commands/theme/version/pull.js +43 -0
  121. package/dist/commands/theme/version/push.d.ts +22 -0
  122. package/dist/commands/theme/version/push.js +123 -0
  123. package/dist/config/project-config.manager.d.ts +3 -3
  124. package/dist/config/project-config.manager.js +22 -31
  125. package/dist/hooks/finally.d.ts +2 -1
  126. package/dist/hooks/finally.js +38 -7
  127. package/dist/lib/api-client.js +2 -1
  128. package/dist/lib/app-build.js +3 -1
  129. package/dist/lib/app-manifest.js +2 -2
  130. package/dist/lib/base.d.ts +2 -2
  131. package/dist/lib/base.js +33 -38
  132. package/dist/lib/declared-type-command.d.ts +35 -0
  133. package/dist/lib/declared-type-command.js +192 -0
  134. package/dist/lib/dev-watch.d.ts +1 -1
  135. package/dist/lib/dev-watch.js +7 -3
  136. package/dist/lib/diff-state.js +1 -0
  137. package/dist/lib/github-release.js +4 -1
  138. package/dist/lib/local-callback-server.d.ts +12 -6
  139. package/dist/lib/local-callback-server.js +24 -23
  140. package/dist/lib/php-files-command.js +3 -1
  141. package/dist/lib/push-command.d.ts +6 -0
  142. package/dist/lib/push-command.js +61 -18
  143. package/dist/lib/put-or-create.js +3 -4
  144. package/dist/lib/resource-state.js +40 -86
  145. package/dist/lib/rollback-materialize.js +13 -24
  146. package/dist/lib/snapshot-store.js +2 -2
  147. package/dist/lib/spinner.d.ts +2 -0
  148. package/dist/lib/spinner.js +72 -0
  149. package/dist/lib/temp-admin.d.ts +10 -1
  150. package/dist/lib/temp-admin.js +27 -2
  151. package/dist/lib/validate-local.js +31 -9
  152. package/dist/lib/wp-authorize-flow.d.ts +10 -17
  153. package/dist/lib/wp-authorize-flow.js +24 -28
  154. package/dist/lib/wp-client.d.ts +1 -0
  155. package/dist/lib/wp-client.js +7 -1
  156. package/dist/lib/wp-site-diagnostic.js +39 -1
  157. package/dist/types/global-config.generated.d.ts +1 -5
  158. package/dist/types/project-config.generated.d.ts +28 -4
  159. package/dist/utils/composer.d.ts +3 -1
  160. package/dist/utils/composer.js +16 -7
  161. package/dist/utils/declared-type-format.d.ts +52 -0
  162. package/dist/utils/declared-type-format.js +233 -0
  163. package/dist/utils/option-format.d.ts +3 -1
  164. package/dist/utils/option-format.js +14 -3
  165. package/dist/utils/page-format.d.ts +5 -1
  166. package/dist/utils/page-format.js +7 -3
  167. package/dist/utils/plugin-sync.js +1 -1
  168. package/dist/utils/resource-dirs.d.ts +9 -3
  169. package/dist/utils/resource-dirs.js +18 -6
  170. package/dist/utils/template-format.d.ts +3 -1
  171. package/dist/utils/template-format.js +10 -3
  172. package/dist/utils/theme-css.d.ts +19 -0
  173. package/dist/utils/theme-css.js +100 -0
  174. package/oclif.manifest.json +3542 -1087
  175. package/package.json +40 -21
  176. package/schemas/global-config.schema.json +2 -6
  177. package/schemas/project-config.schema.json +37 -7
  178. package/dist/commands/seo/diff.js +0 -5
  179. package/dist/commands/seo/list.d.ts +0 -18
  180. package/dist/commands/seo/list.js +0 -58
  181. package/dist/commands/seo/pull.d.ts +0 -17
  182. package/dist/commands/seo/pull.js +0 -87
  183. package/dist/commands/seo/push.d.ts +0 -23
  184. package/dist/commands/seo/push.js +0 -176
  185. package/dist/commands/seo/rollback.js +0 -5
  186. package/dist/commands/template/push.js +0 -42
  187. package/dist/utils/seo-format.d.ts +0 -29
  188. package/dist/utils/seo-format.js +0 -18
  189. /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
  190. /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
@@ -8,15 +8,15 @@ export declare class ProjectConfigManager {
8
8
  id: string;
9
9
  });
10
10
  getConfigFilePath(): string;
11
- getCurrentEnv(): EnvironmentConfig | null;
12
11
  getCurrentProject(): null | (ProjectConfig & {
13
12
  id: string;
14
13
  });
14
+ getDefaultEnvironment(projectId: string): EnvironmentConfig | null;
15
15
  getEnvironment(projectId: string, envName: string): EnvironmentConfig | null;
16
16
  getProject(id: string): null | ProjectConfig;
17
17
  isTelemetryDisabled(): boolean;
18
18
  listEnvironments(projectId: string): Array<EnvironmentConfig & {
19
- isCurrent: boolean;
19
+ isDefault: boolean;
20
20
  }>;
21
21
  listProjects(): Array<ProjectConfig & {
22
22
  id: string;
@@ -26,7 +26,7 @@ export declare class ProjectConfigManager {
26
26
  removeEnvironment(projectId: string, envName: string): void;
27
27
  removeProject(id: string): void;
28
28
  setConfigDir(configDir: string): void;
29
- setCurrent(projectId: string, envName: string): void;
29
+ setCurrent(projectId: string): void;
30
30
  setEnvironment(projectId: string, envName: string, env: EnvironmentConfig): void;
31
31
  setEnvironmentApiId(projectId: string, envName: string, apiEnvironmentId: string): void;
32
32
  setProject(id: string, project: ProjectConfig): void;
@@ -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 config = this.readConfig();
75
- const project = config.projects[projectId];
77
+ const project = this.getProject(projectId);
76
78
  if (!project)
77
79
  return [];
78
- return Object.values(project.environments).map((env) => ({
79
- ...env,
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
- const nextProject = nextId ? config.projects[nextId] : undefined;
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, envName) {
121
+ setCurrent(projectId) {
128
122
  const config = this.readConfig();
129
123
  if (!Object.hasOwn(config.projects, projectId))
130
124
  return;
131
- config.currentProject = { env: envName, id: projectId };
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 ??= { env: envName, id: projectId };
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
- if (!config.currentProject) {
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
- return typeof pointer.id === 'string' && typeof pointer.env === 'string' ? { env: pointer.env, id: pointer.id } : null;
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)
@@ -1,3 +1,4 @@
1
- import type { Hook } from '@oclif/core';
1
+ import { type Hook } from '@oclif/core';
2
2
  declare const hook: Hook.Finally;
3
+ export declare function dropHttpCause(error: Error): void;
3
4
  export default hook;
@@ -1,3 +1,5 @@
1
+ import { settings } from '@oclif/core';
2
+ import { RequestError } from 'got';
1
3
  import { isTelemetryDisabled, redactArgv, resolveEnvironment, runtimeContext, scrubEvent, SENTRY_DSN, } from '../lib/sentry.js';
2
4
  // oclif has no `command_error` hook (checked @oclif/core@4.11.11's hooks.d.ts). `finally`
3
5
  // is the closest equivalent: it always runs at the end of the CLI lifecycle and carries
@@ -7,8 +9,15 @@ import { isTelemetryDisabled, redactArgv, resolveEnvironment, runtimeContext, sc
7
9
  // It's a heavy module (@opentelemetry deps, import-in-the-middle instrumentation), so loading
8
10
  // it eagerly on every command would tax the common case where commands succeed.
9
11
  const hook = async function (options) {
10
- if (!options.error || isTelemetryDisabled())
12
+ if (!options.error)
11
13
  return;
14
+ if (!isTelemetryDisabled())
15
+ await report(this, options);
16
+ // After the report, so Sentry still gets the whole chain. LPS_DEBUG=1 keeps it on screen too.
17
+ if (!settings.debug)
18
+ dropHttpCause(options.error);
19
+ };
20
+ async function report(context, options) {
12
21
  try {
13
22
  const Sentry = await import('@sentry/node');
14
23
  Sentry.init({
@@ -17,11 +26,19 @@ const hook = async function (options) {
17
26
  beforeSend: scrubEvent,
18
27
  dsn: SENTRY_DSN,
19
28
  environment: resolveEnvironment(),
20
- release: this.config.version,
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
+ },
21
40
  // Node defaults `server_name` to os.hostname(), which is often the user's real name
22
- // (e.g. "jane-doe-macbook-pro"). sendDefaultPii covers IP addresses and similar, off by
23
- // default but set explicitly since this reports from users' own machines.
24
- sendDefaultPii: false,
41
+ // (e.g. "jane-doe-macbook-pro").
25
42
  serverName: 'loopress',
26
43
  });
27
44
  Sentry.captureException(options.error, {
@@ -32,7 +49,21 @@ const hook = async function (options) {
32
49
  await Sentry.flush(2000);
33
50
  }
34
51
  catch (error) {
35
- this.debug('Failed to report error to Sentry: %O', error);
52
+ context.debug('Failed to report error to Sentry: %O', error);
36
53
  }
37
- };
54
+ }
55
+ // WpClient and ApiClient already fold what matters of a got error (status, the server's own
56
+ // reason) into their own message, and keep got's error as `cause` for the helpers that read
57
+ // its response. oclif prints every cause after the message, so without this a clear "Request
58
+ // failed (409) on ...: The active theme is not a block theme" ends with "Caused by: HTTPError:
59
+ // Request failed with status code 409 ... Code: ERR_NON_2XX_3XX_RESPONSE", which is what the
60
+ // user remembers. Other causes (a failed plugin install under its fallback advice) stay.
61
+ export function dropHttpCause(error) {
62
+ for (let current = error; current instanceof Error; current = current.cause) {
63
+ if (current.cause instanceof RequestError) {
64
+ delete current.cause;
65
+ return;
66
+ }
67
+ }
68
+ }
38
69
  export default hook;
@@ -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) {
@@ -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 { code, output } = await new Promise((resolve, reject) => {
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'))
@@ -22,7 +22,7 @@ export function isAllowedAsset(relPath) {
22
22
  // server would reject, so the push fails locally with a clear message instead of a 400
23
23
  // halfway through the upload.
24
24
  export async function buildFileList(distDir) {
25
- const relPaths = (await walkFiles(distDir)).sort((a, b) => a.localeCompare(b));
25
+ const relPaths = (await walkFiles(distDir)).toSorted((a, b) => a.localeCompare(b));
26
26
  const files = [];
27
27
  for (const path of relPaths) {
28
28
  if (!isAllowedAsset(path)) {
@@ -37,7 +37,7 @@ export async function buildFileList(distDir) {
37
37
  // Used as the enqueue `?ver` (cache busting) and as the build identity in `app list`.
38
38
  export function computeBuildId(files) {
39
39
  const digest = createHash('sha256');
40
- for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
40
+ for (const file of files.toSorted((a, b) => a.path.localeCompare(b.path))) {
41
41
  digest.update(`${file.path}:${file.sha256}\n`);
42
42
  }
43
43
  return digest.digest('hex').slice(0, 12);
@@ -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 productionConfirmed: boolean;
17
18
  protected projectId: string;
18
19
  protected siteConfig: EnvironmentConfig;
19
20
  protected yes: boolean;
@@ -38,10 +39,9 @@ export declare abstract class LoopressCommand extends Command {
38
39
  protected resolveHooksPath(override?: string): string;
39
40
  protected resolveMenuPath(override?: string): string;
40
41
  protected resolveOptionsPath(override?: string): string;
41
- protected resolveSeoPath(override?: string): string;
42
42
  protected resolveSnippetsPath(override?: string): string;
43
43
  protected resolveThemeStylesPath(override?: string): string;
44
+ protected warnRemovedSeoModule(): void;
44
45
  private pickEnvironment;
45
46
  private resolveEnvironment;
46
- private resolveEnvironmentForConfiguredProject;
47
47
  }
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: targeting an environment explicitly beats depending on
15
- // the machine-wide mutable state of `lps project switch`, which is shared across terminals.
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, overriding the globally active one (lps project switch)',
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 hidden --production-confirmed, see there.
32
+ productionConfirmed = false;
29
33
  projectId;
30
34
  siteConfig;
31
35
  yes = false;
@@ -54,6 +58,7 @@ export class LoopressCommand extends Command {
54
58
  }));
55
59
  this.dryRun = Boolean(flags['dry-run']);
56
60
  this.yes = Boolean(flags.yes);
61
+ this.productionConfirmed = Boolean(flags['production-confirmed']);
57
62
  this.localConfig = await readLocalConfig();
58
63
  const resolved = this.resolveEnvironment(flags.env);
59
64
  this.siteConfig = resolved.env;
@@ -104,13 +109,15 @@ export class LoopressCommand extends Command {
104
109
  }
105
110
  if (options.alwaysCreateDir === true || items.length > 0)
106
111
  await mkdir(dir, { recursive: true });
107
- await new Listr(items.map((item) => ({
108
- async task(_ctx, task) {
109
- await options.write(item, dir);
110
- task.output = `Pulled: ${options.title(item)}`;
111
- },
112
- title: `Pull ${options.title(item)}`,
113
- })), { renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
112
+ await withoutSpinner(async () => {
113
+ await new Listr(items.map((item) => ({
114
+ async task(_ctx, task) {
115
+ await options.write(item, dir);
116
+ task.output = `Pulled: ${options.title(item)}`;
117
+ },
118
+ title: `Pull ${options.title(item)}`,
119
+ })), { renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
120
+ });
114
121
  await this.removeOrphanedFiles(dir, orphans, options.orphanReason);
115
122
  this.log(options.pulledMessage);
116
123
  }
@@ -162,15 +169,19 @@ export class LoopressCommand extends Command {
162
169
  resolveOptionsPath(override) {
163
170
  return resolveResourceDir('options', this.localConfig, override);
164
171
  }
165
- resolveSeoPath(override) {
166
- return resolveResourceDir('seo', this.localConfig, override);
167
- }
168
172
  resolveSnippetsPath(override) {
169
173
  return resolveResourceDir('snippets', this.localConfig, override);
170
174
  }
171
175
  resolveThemeStylesPath(override) {
172
176
  return resolveResourceDir('themeStyles', this.localConfig, override);
173
177
  }
178
+ // `lps seo` was removed: a leftover seo/ folder or seoDir key would otherwise be silently
179
+ // ignored by `lps pull` / `lps push`. Remove once projects have had a release to migrate.
180
+ warnRemovedSeoModule() {
181
+ if (!('seoDir' in this.localConfig) && !existsSync(join(this.rootDir, 'seo')))
182
+ return;
183
+ 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.');
184
+ }
174
185
  pickEnvironment(project, envName) {
175
186
  const env = project.environments[envName];
176
187
  if (!env) {
@@ -178,24 +189,12 @@ export class LoopressCommand extends Command {
178
189
  }
179
190
  return env;
180
191
  }
192
+ // Order: --env, then the project's default (see getDefaultEnvironment). The project comes from
193
+ // loopress.json when pinned, from `lps project switch` otherwise.
181
194
  resolveEnvironment(envName) {
182
- if (this.localConfig.projectId) {
183
- return this.resolveEnvironmentForConfiguredProject(this.localConfig.projectId, envName);
184
- }
185
- if (envName) {
186
- const current = configManager.getCurrentProject();
187
- if (!current) {
188
- this.error('No project configured. Run `lps project config` first.');
189
- }
190
- return { env: this.pickEnvironment(current, envName), projectId: current.id };
191
- }
192
- const env = configManager.getCurrentEnv();
193
- const current = configManager.getCurrentProject();
194
- if (env && current)
195
- return { env, projectId: current.id };
196
- this.error('No environment configured. Run `lps project config` first.');
197
- }
198
- resolveEnvironmentForConfiguredProject(projectId, envName) {
195
+ const projectId = this.localConfig.projectId ?? configManager.getCurrentProject()?.id;
196
+ if (!projectId)
197
+ this.error('No project configured. Run `lps project config` first.');
199
198
  const project = configManager.getProject(projectId);
200
199
  if (!project) {
201
200
  this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
@@ -206,14 +205,10 @@ export class LoopressCommand extends Command {
206
205
  }
207
206
  if (envName)
208
207
  return { env: this.pickEnvironment(project, envName), projectId };
209
- if (envNames.length === 1) {
210
- return { env: project.environments[envNames[0]], projectId };
211
- }
212
- const current = configManager.getCurrentProject();
213
- const currentEnv = current?.id === projectId ? configManager.getCurrentEnv() : null;
214
- if (!currentEnv) {
215
- this.error(`Project "${project.name}" has multiple environments. Run \`lps project switch\` to pick one.`);
208
+ const env = configManager.getDefaultEnvironment(projectId);
209
+ if (!env) {
210
+ this.error(`Project "${project.name}" has no "local" environment, pass --env to pick one. Available: ${envNames.join(', ')}`);
216
211
  }
217
- return { env: currentEnv, projectId };
212
+ return { env, projectId };
218
213
  }
219
214
  }
@@ -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 {};