@loopress/cli 0.11.0 → 0.13.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 +45 -20
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +21 -90
- package/dist/commands/project/config.d.ts +2 -0
- package/dist/commands/project/config.js +45 -11
- package/dist/commands/project/sync.js +15 -5
- package/dist/commands/snippet/publish.d.ts +10 -0
- package/dist/commands/snippet/publish.js +74 -0
- package/dist/commands/snippet/pull.d.ts +1 -0
- package/dist/commands/snippet/pull.js +38 -4
- package/dist/commands/snippet/push.js +5 -61
- package/dist/config/project-config.manager.d.ts +3 -0
- package/dist/config/project-config.manager.js +12 -0
- package/dist/lib/load-snippets.d.ts +2 -0
- package/dist/lib/load-snippets.js +84 -0
- package/dist/lib/local-callback-server.d.ts +25 -0
- package/dist/lib/local-callback-server.js +93 -0
- package/dist/lib/open-browser.d.ts +2 -0
- package/dist/lib/open-browser.js +12 -0
- package/dist/lib/wp-authorize-flow.d.ts +10 -0
- package/dist/lib/wp-authorize-flow.js +53 -0
- package/dist/lib/wp-client.js +18 -1
- package/dist/lib/wp-site-diagnostic.d.ts +13 -0
- package/dist/lib/wp-site-diagnostic.js +46 -0
- package/oclif.manifest.json +138 -108
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ $ npm install -g @loopress/cli
|
|
|
21
21
|
$ lps COMMAND
|
|
22
22
|
running command...
|
|
23
23
|
$ lps (--version)
|
|
24
|
-
@loopress/cli/0.
|
|
24
|
+
@loopress/cli/0.13.0 linux-x64 node-v24.18.0
|
|
25
25
|
$ lps --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ lps COMMAND
|
|
@@ -61,6 +61,7 @@ report.
|
|
|
61
61
|
* [`lps project switch`](#lps-project-switch)
|
|
62
62
|
* [`lps project sync`](#lps-project-sync)
|
|
63
63
|
* [`lps snippet list`](#lps-snippet-list)
|
|
64
|
+
* [`lps snippet publish [PATH]`](#lps-snippet-publish-path)
|
|
64
65
|
* [`lps snippet pull [PATH]`](#lps-snippet-pull-path)
|
|
65
66
|
* [`lps snippet push [PATH]`](#lps-snippet-push-path)
|
|
66
67
|
* [`lps status`](#lps-status)
|
|
@@ -85,7 +86,7 @@ EXAMPLES
|
|
|
85
86
|
$ lps composer pull --dry-run
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
89
|
+
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/composer/pull.ts)_
|
|
89
90
|
|
|
90
91
|
## `lps composer push`
|
|
91
92
|
|
|
@@ -107,7 +108,7 @@ EXAMPLES
|
|
|
107
108
|
$ lps composer push --dry-run
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
111
|
+
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/composer/push.ts)_
|
|
111
112
|
|
|
112
113
|
## `lps help [COMMAND]`
|
|
113
114
|
|
|
@@ -144,7 +145,7 @@ EXAMPLES
|
|
|
144
145
|
$ lps init
|
|
145
146
|
```
|
|
146
147
|
|
|
147
|
-
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.
|
|
148
|
+
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/init.ts)_
|
|
148
149
|
|
|
149
150
|
## `lps login`
|
|
150
151
|
|
|
@@ -161,7 +162,7 @@ EXAMPLES
|
|
|
161
162
|
$ lps login
|
|
162
163
|
```
|
|
163
164
|
|
|
164
|
-
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.
|
|
165
|
+
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/login.ts)_
|
|
165
166
|
|
|
166
167
|
## `lps logout`
|
|
167
168
|
|
|
@@ -178,7 +179,7 @@ EXAMPLES
|
|
|
178
179
|
$ lps logout
|
|
179
180
|
```
|
|
180
181
|
|
|
181
|
-
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.
|
|
182
|
+
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/logout.ts)_
|
|
182
183
|
|
|
183
184
|
## `lps plugin add SLUG [VERSION]`
|
|
184
185
|
|
|
@@ -206,7 +207,7 @@ EXAMPLES
|
|
|
206
207
|
$ lps plugin add contact-form-7 --dry-run
|
|
207
208
|
```
|
|
208
209
|
|
|
209
|
-
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.
|
|
210
|
+
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/plugin/add.ts)_
|
|
210
211
|
|
|
211
212
|
## `lps plugin pull`
|
|
212
213
|
|
|
@@ -228,7 +229,7 @@ EXAMPLES
|
|
|
228
229
|
$ lps plugin pull --dry-run
|
|
229
230
|
```
|
|
230
231
|
|
|
231
|
-
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
232
|
+
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/plugin/pull.ts)_
|
|
232
233
|
|
|
233
234
|
## `lps plugin push`
|
|
234
235
|
|
|
@@ -250,24 +251,25 @@ EXAMPLES
|
|
|
250
251
|
$ lps plugin push --dry-run
|
|
251
252
|
```
|
|
252
253
|
|
|
253
|
-
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
254
|
+
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/plugin/push.ts)_
|
|
254
255
|
|
|
255
256
|
## `lps project config`
|
|
256
257
|
|
|
257
|
-
Add or update a WordPress project environment
|
|
258
|
+
Add or update a WordPress project environment. By default, authorizes via WordPress in your browser; manual username/Application Password entry is available as a fallback.
|
|
258
259
|
|
|
259
260
|
```
|
|
260
261
|
USAGE
|
|
261
262
|
$ lps project config
|
|
262
263
|
|
|
263
264
|
DESCRIPTION
|
|
264
|
-
Add or update a WordPress project environment
|
|
265
|
+
Add or update a WordPress project environment. By default, authorizes via WordPress in your browser; manual
|
|
266
|
+
username/Application Password entry is available as a fallback.
|
|
265
267
|
|
|
266
268
|
EXAMPLES
|
|
267
269
|
$ lps project config
|
|
268
270
|
```
|
|
269
271
|
|
|
270
|
-
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.
|
|
272
|
+
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/project/config.ts)_
|
|
271
273
|
|
|
272
274
|
## `lps project list`
|
|
273
275
|
|
|
@@ -284,7 +286,7 @@ EXAMPLES
|
|
|
284
286
|
$ lps project list
|
|
285
287
|
```
|
|
286
288
|
|
|
287
|
-
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
289
|
+
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/project/list.ts)_
|
|
288
290
|
|
|
289
291
|
## `lps project remove`
|
|
290
292
|
|
|
@@ -301,7 +303,7 @@ EXAMPLES
|
|
|
301
303
|
$ lps project remove
|
|
302
304
|
```
|
|
303
305
|
|
|
304
|
-
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.
|
|
306
|
+
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/project/remove.ts)_
|
|
305
307
|
|
|
306
308
|
## `lps project switch`
|
|
307
309
|
|
|
@@ -318,7 +320,7 @@ EXAMPLES
|
|
|
318
320
|
$ lps project switch
|
|
319
321
|
```
|
|
320
322
|
|
|
321
|
-
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.
|
|
323
|
+
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/project/switch.ts)_
|
|
322
324
|
|
|
323
325
|
## `lps project sync`
|
|
324
326
|
|
|
@@ -335,7 +337,7 @@ EXAMPLES
|
|
|
335
337
|
$ lps project sync
|
|
336
338
|
```
|
|
337
339
|
|
|
338
|
-
_See code: [src/commands/project/sync.ts](https://github.com/loopress/loopress/blob/v0.
|
|
340
|
+
_See code: [src/commands/project/sync.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/project/sync.ts)_
|
|
339
341
|
|
|
340
342
|
## `lps snippet list`
|
|
341
343
|
|
|
@@ -355,7 +357,30 @@ EXAMPLES
|
|
|
355
357
|
$ lps snippet list
|
|
356
358
|
```
|
|
357
359
|
|
|
358
|
-
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
360
|
+
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/snippet/list.ts)_
|
|
361
|
+
|
|
362
|
+
## `lps snippet publish [PATH]`
|
|
363
|
+
|
|
364
|
+
Publish snippets to your Loopress account so they can be deployed to other projects. Does not touch any WordPress site.
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
USAGE
|
|
368
|
+
$ lps snippet publish [PATH]
|
|
369
|
+
|
|
370
|
+
ARGUMENTS
|
|
371
|
+
[PATH] Path to snippets directory (overrides project config)
|
|
372
|
+
|
|
373
|
+
DESCRIPTION
|
|
374
|
+
Publish snippets to your Loopress account so they can be deployed to other projects. Does not touch any WordPress
|
|
375
|
+
site.
|
|
376
|
+
|
|
377
|
+
EXAMPLES
|
|
378
|
+
$ lps snippet publish
|
|
379
|
+
|
|
380
|
+
$ lps snippet publish --path ./snippets
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
_See code: [src/commands/snippet/publish.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/snippet/publish.ts)_
|
|
359
384
|
|
|
360
385
|
## `lps snippet pull [PATH]`
|
|
361
386
|
|
|
@@ -380,7 +405,7 @@ EXAMPLES
|
|
|
380
405
|
$ lps snippet pull --path ./snippets
|
|
381
406
|
```
|
|
382
407
|
|
|
383
|
-
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
408
|
+
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/snippet/pull.ts)_
|
|
384
409
|
|
|
385
410
|
## `lps snippet push [PATH]`
|
|
386
411
|
|
|
@@ -406,7 +431,7 @@ EXAMPLES
|
|
|
406
431
|
$ lps snippet push --path ./snippets
|
|
407
432
|
```
|
|
408
433
|
|
|
409
|
-
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
434
|
+
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/snippet/push.ts)_
|
|
410
435
|
|
|
411
436
|
## `lps status`
|
|
412
437
|
|
|
@@ -423,5 +448,5 @@ EXAMPLES
|
|
|
423
448
|
$ lps status
|
|
424
449
|
```
|
|
425
450
|
|
|
426
|
-
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.
|
|
451
|
+
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.13.0/src/commands/status.ts)_
|
|
427
452
|
<!-- commandsstop -->
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { exec } from 'node:child_process';
|
|
3
|
-
import { createServer } from 'node:http';
|
|
4
2
|
import { authManager } from '../config/auth.manager.js';
|
|
3
|
+
import { renderResultPage, waitForLocalCallback } from '../lib/local-callback-server.js';
|
|
5
4
|
const CONSOLE_URL = 'https://console.loopress.dev';
|
|
6
|
-
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
5
|
export default class Login extends Command {
|
|
8
6
|
static description = 'Log in to the Loopress console';
|
|
9
7
|
static examples = ['$ lps login'];
|
|
@@ -12,95 +10,28 @@ export default class Login extends Command {
|
|
|
12
10
|
authManager.setAuth({ email, savedAt: new Date().toISOString(), token });
|
|
13
11
|
this.log(`\nLogged in${email ? ` as ${email}` : ''}. You're all set!`);
|
|
14
12
|
}
|
|
15
|
-
openBrowser(url) {
|
|
16
|
-
const cmds = {
|
|
17
|
-
darwin: `open "${url}"`,
|
|
18
|
-
linux: `xdg-open "${url}"`,
|
|
19
|
-
win32: `start "" "${url}"`,
|
|
20
|
-
};
|
|
21
|
-
const cmd = cmds[process.platform];
|
|
22
|
-
if (cmd)
|
|
23
|
-
exec(cmd);
|
|
24
|
-
}
|
|
25
13
|
waitForCallback() {
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
res.end('Missing token');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
-
res.end(SUCCESS_PAGE);
|
|
39
|
-
clearTimeout(timer);
|
|
40
|
-
server.close();
|
|
41
|
-
resolve({ email, token });
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
res.writeHead(500);
|
|
45
|
-
res.end('Internal error');
|
|
46
|
-
server.close();
|
|
47
|
-
reject(error);
|
|
14
|
+
return waitForLocalCallback({
|
|
15
|
+
buildUrl: (callbackBaseUrl) => `${CONSOLE_URL}/cli-auth?callbackUrl=${encodeURIComponent(`${callbackBaseUrl}/callback`)}`,
|
|
16
|
+
handleRequest(url, { resolveWithPage, respondBadRequest }) {
|
|
17
|
+
const token = url.searchParams.get('token');
|
|
18
|
+
const email = url.searchParams.get('email') ?? undefined;
|
|
19
|
+
if (!token) {
|
|
20
|
+
respondBadRequest('Missing token');
|
|
21
|
+
return;
|
|
48
22
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const timer = setTimeout(() => {
|
|
55
|
-
server.close();
|
|
56
|
-
reject(new Error('Login timed out after 5 minutes. Please try again.'));
|
|
57
|
-
}, TIMEOUT_MS);
|
|
58
|
-
server.listen(0, '127.0.0.1', () => {
|
|
59
|
-
const { port } = server.address();
|
|
60
|
-
const callbackUrl = `http://localhost:${port}/callback`;
|
|
61
|
-
const loginUrl = `${CONSOLE_URL}/cli-auth?callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
|
62
|
-
this.log('Opening Loopress console in your browser...');
|
|
63
|
-
this.log(`\nIf it doesn't open automatically, visit:\n${loginUrl}\n`);
|
|
64
|
-
this.openBrowser(loginUrl);
|
|
65
|
-
});
|
|
23
|
+
resolveWithPage(SUCCESS_PAGE, { email, token });
|
|
24
|
+
},
|
|
25
|
+
log: (message) => this.log(message),
|
|
26
|
+
openingMessage: 'Opening Loopress console in your browser...',
|
|
27
|
+
timeoutMessage: 'Login timed out after 5 minutes. Please try again.',
|
|
66
28
|
});
|
|
67
29
|
}
|
|
68
30
|
}
|
|
69
|
-
const SUCCESS_PAGE =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
77
|
-
body {
|
|
78
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
79
|
-
background: #f0fdf4;
|
|
80
|
-
display: flex;
|
|
81
|
-
align-items: center;
|
|
82
|
-
justify-content: center;
|
|
83
|
-
min-height: 100dvh;
|
|
84
|
-
}
|
|
85
|
-
.card {
|
|
86
|
-
background: #fff;
|
|
87
|
-
border-radius: 16px;
|
|
88
|
-
padding: 2.5rem 3rem;
|
|
89
|
-
text-align: center;
|
|
90
|
-
box-shadow: 0 4px 32px rgba(0, 0, 0, .08);
|
|
91
|
-
max-width: 420px;
|
|
92
|
-
width: 90%;
|
|
93
|
-
}
|
|
94
|
-
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
95
|
-
h1 { color: #15803d; font-size: 1.5rem; margin-bottom: .5rem; }
|
|
96
|
-
p { color: #6b7280; font-size: .95rem; line-height: 1.5; }
|
|
97
|
-
</style>
|
|
98
|
-
</head>
|
|
99
|
-
<body>
|
|
100
|
-
<div class="card">
|
|
101
|
-
<div class="icon">✅</div>
|
|
102
|
-
<h1>Login successful!</h1>
|
|
103
|
-
<p>You can close this tab and return to your terminal.</p>
|
|
104
|
-
</div>
|
|
105
|
-
</body>
|
|
106
|
-
</html>`;
|
|
31
|
+
const SUCCESS_PAGE = renderResultPage({
|
|
32
|
+
background: '#f0fdf4',
|
|
33
|
+
heading: 'Login successful!',
|
|
34
|
+
headingColor: '#15803d',
|
|
35
|
+
icon: '✅',
|
|
36
|
+
tabTitle: 'Logged in',
|
|
37
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { confirm, input, password as passwordPrompt, select } from '@inquirer/prompts';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
+
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
5
|
+
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
4
6
|
const NEW_PROJECT = '__new__';
|
|
7
|
+
const AUTH_BROWSER = 'browser';
|
|
8
|
+
const AUTH_MANUAL = 'manual';
|
|
5
9
|
export default class Config extends Command {
|
|
6
|
-
static description = 'Add or update a WordPress project environment';
|
|
10
|
+
static description = 'Add or update a WordPress project environment. By default, authorizes via WordPress in your browser; manual username/Application Password entry is available as a fallback.';
|
|
7
11
|
static examples = ['$ lps project config'];
|
|
8
12
|
async run() {
|
|
9
13
|
await this.parse(Config);
|
|
@@ -34,7 +38,7 @@ export default class Config extends Command {
|
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
const
|
|
41
|
+
const rawUrl = await input({
|
|
38
42
|
message: 'WordPress URL',
|
|
39
43
|
validate(value) {
|
|
40
44
|
try {
|
|
@@ -49,15 +53,8 @@ export default class Config extends Command {
|
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
});
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
validate: (value) => (value.trim().length > 0 ? true : 'Username cannot be empty'),
|
|
55
|
-
});
|
|
56
|
-
const appPassword = await passwordPrompt({
|
|
57
|
-
mask: '*',
|
|
58
|
-
message: 'Application password',
|
|
59
|
-
validate: (value) => (value.trim().length > 0 ? true : 'Application password cannot be empty'),
|
|
60
|
-
});
|
|
56
|
+
const url = rawUrl.replace(/\/+$/, '');
|
|
57
|
+
const { appPassword, user } = await this.resolveCredentials(url);
|
|
61
58
|
const token = `${user}:${appPassword}`;
|
|
62
59
|
const env = {
|
|
63
60
|
addedAt: new Date().toISOString(),
|
|
@@ -79,6 +76,43 @@ export default class Config extends Command {
|
|
|
79
76
|
this.log(`✓ "${projectName}/${envName}" configured`);
|
|
80
77
|
this.log('→ Run `lps project switch` to change the active project or environment');
|
|
81
78
|
}
|
|
79
|
+
async promptManualCredentials() {
|
|
80
|
+
const user = await input({
|
|
81
|
+
message: 'Username',
|
|
82
|
+
validate: (value) => (value.trim().length > 0 ? true : 'Username cannot be empty'),
|
|
83
|
+
});
|
|
84
|
+
const appPassword = await passwordPrompt({
|
|
85
|
+
mask: '*',
|
|
86
|
+
message: 'Application password',
|
|
87
|
+
validate: (value) => (value.trim().length > 0 ? true : 'Application password cannot be empty'),
|
|
88
|
+
});
|
|
89
|
+
return { appPassword, user };
|
|
90
|
+
}
|
|
91
|
+
async resolveCredentials(url) {
|
|
92
|
+
const authMode = await select({
|
|
93
|
+
choices: [
|
|
94
|
+
{ name: 'Authorize in my browser (recommended)', value: AUTH_BROWSER },
|
|
95
|
+
{ name: 'Enter credentials manually', value: AUTH_MANUAL },
|
|
96
|
+
],
|
|
97
|
+
message: 'How do you want to authenticate?',
|
|
98
|
+
});
|
|
99
|
+
if (authMode === AUTH_MANUAL) {
|
|
100
|
+
return this.promptManualCredentials();
|
|
101
|
+
}
|
|
102
|
+
const diagnostic = await diagnoseWpSite(url);
|
|
103
|
+
if (!diagnostic.ok) {
|
|
104
|
+
this.warn(`${diagnostic.reason}\nFalling back to manual credential entry.`);
|
|
105
|
+
return this.promptManualCredentials();
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const { password, userLogin } = await authorizeWithBrowser(url, (message) => this.log(message));
|
|
109
|
+
return { appPassword: password, user: userLogin };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.warn(`${error.message}\nFalling back to manual credential entry.`);
|
|
113
|
+
return this.promptManualCredentials();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
async resolveProject() {
|
|
83
117
|
const projects = configManager.listProjects();
|
|
84
118
|
if (projects.length > 0) {
|
|
@@ -184,12 +184,22 @@ export default class Sync extends Command {
|
|
|
184
184
|
return { action: 'create', project };
|
|
185
185
|
}
|
|
186
186
|
pullProject(apiProject, task) {
|
|
187
|
-
|
|
187
|
+
// Reuse a local project already linked to this API project instead of always minting a
|
|
188
|
+
// new one: otherwise every sync run that can't "claim" an existing link (e.g. after the
|
|
189
|
+
// local config was reset or desynced from the API) creates yet another duplicate entry.
|
|
190
|
+
const existing = configManager.findProjectByApiId(apiProject.id);
|
|
191
|
+
const environments = { ...existing?.environments };
|
|
188
192
|
for (const env of apiProject.environments) {
|
|
189
|
-
environments[env.name] = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
environments[env.name] = {
|
|
194
|
+
...environments[env.name],
|
|
195
|
+
addedAt: environments[env.name]?.addedAt ?? env.createdAt,
|
|
196
|
+
apiEnvironmentId: env.id,
|
|
197
|
+
name: env.name,
|
|
198
|
+
url: env.url,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
configManager.setProject(existing?.id ?? configManager.createProjectId(apiProject.name), {
|
|
202
|
+
addedAt: existing?.addedAt ?? apiProject.createdAt,
|
|
193
203
|
apiProjectId: apiProject.id,
|
|
194
204
|
environments,
|
|
195
205
|
name: apiProject.name,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Publish extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private toPayload;
|
|
10
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import slugify from 'slugify';
|
|
4
|
+
import { authManager } from '../../config/auth.manager.js';
|
|
5
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
6
|
+
import { ApiClient } from '../../lib/api-client.js';
|
|
7
|
+
import { loadSnippets } from '../../lib/load-snippets.js';
|
|
8
|
+
import { readLocalConfig } from '../../utils/loopress-config.js';
|
|
9
|
+
// Publishes to the Loopress api (not a WordPress site), so this does not extend
|
|
10
|
+
// `LoopressCommand`/`PushCommand`: those force an environment to be resolved, but a project
|
|
11
|
+
// publishing its snippets for sharing doesn't need one, a pure "library" project
|
|
12
|
+
// may have zero environments configured at all.
|
|
13
|
+
export default class Publish extends Command {
|
|
14
|
+
static args = {
|
|
15
|
+
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
16
|
+
};
|
|
17
|
+
static description = 'Publish snippets to your Loopress account so they can be deployed to other projects. Does not touch any WordPress site.';
|
|
18
|
+
static examples = ['$ lps snippet publish', '$ lps snippet publish --path ./snippets'];
|
|
19
|
+
async run() {
|
|
20
|
+
const { args } = await this.parse(Publish);
|
|
21
|
+
const token = process.env.LOOPRESS_TOKEN ?? authManager.getAuth()?.token;
|
|
22
|
+
if (!token) {
|
|
23
|
+
this.error('Not logged in. Run `lps login` first.');
|
|
24
|
+
}
|
|
25
|
+
const localConfig = await readLocalConfig();
|
|
26
|
+
const projectId = localConfig.projectId ?? configManager.getCurrentProject()?.id;
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
this.error('No project configured. Run `lps project config` first.');
|
|
29
|
+
}
|
|
30
|
+
const project = configManager.getProject(projectId);
|
|
31
|
+
if (!project) {
|
|
32
|
+
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
33
|
+
}
|
|
34
|
+
if (!project.apiProjectId) {
|
|
35
|
+
this.error(`Project "${project.name}" is not linked to your Loopress account yet. Run \`lps project sync\` first.`);
|
|
36
|
+
}
|
|
37
|
+
const path = args.path ?? join(localConfig.rootDir ?? '.', localConfig.snippetsDir ?? 'snippets');
|
|
38
|
+
this.log(`Publishing snippets from ${path}`);
|
|
39
|
+
let snippets;
|
|
40
|
+
try {
|
|
41
|
+
snippets = await loadSnippets(path);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.error(error.message);
|
|
45
|
+
}
|
|
46
|
+
const api = new ApiClient(token);
|
|
47
|
+
try {
|
|
48
|
+
await api.post(`projects/${project.apiProjectId}/snippets/publish`, {
|
|
49
|
+
snippets: snippets.map((snippet) => this.toPayload(snippet)),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.error(error.message);
|
|
54
|
+
}
|
|
55
|
+
this.log(`Published ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to your Loopress account.`);
|
|
56
|
+
}
|
|
57
|
+
toPayload(snippet) {
|
|
58
|
+
return {
|
|
59
|
+
active: snippet.active,
|
|
60
|
+
code: snippet.code,
|
|
61
|
+
insertMethod: snippet.insertMethod,
|
|
62
|
+
location: snippet.location,
|
|
63
|
+
name: snippet.name,
|
|
64
|
+
priority: snippet.priority,
|
|
65
|
+
shortcodeAttributes: snippet.shortcodeAttributes,
|
|
66
|
+
// Derived from the snippet's name rather than its on-disk filename: the same
|
|
67
|
+
// slugification `push`/`pull` already use for the canonical `<id>-<slug>` filename
|
|
68
|
+
// convention, so it stays stable even for a not-yet-pushed file with an arbitrary name.
|
|
69
|
+
slug: slugify(snippet.name, { lower: true, strict: true }),
|
|
70
|
+
tags: snippet.tags,
|
|
71
|
+
type: snippet.type,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { Listr } from 'listr2';
|
|
3
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { extname, join } from 'node:path';
|
|
5
5
|
import slugify from 'slugify';
|
|
6
6
|
import { LoopressCommand } from '../../lib/base.js';
|
|
7
7
|
import { normalizeSnippet, SNIPPETS_ENDPOINT } from '../../utils/snippet-format.js';
|
|
@@ -55,13 +55,20 @@ export default class Pull extends LoopressCommand {
|
|
|
55
55
|
this.log(`Snippets path: ${path}`);
|
|
56
56
|
const remoteList = await this.wp.get(SNIPPETS_ENDPOINT);
|
|
57
57
|
const snippets = remoteList.map((r) => normalizeSnippet(r));
|
|
58
|
+
const pullable = snippets.filter((snippet) => snippet.name.trim());
|
|
59
|
+
const skipped = snippets.length - pullable.length;
|
|
60
|
+
// Files following the `<id>-<slug>` convention whose id is no longer in the current
|
|
61
|
+
// remote list belong to a snippet that was deleted on WordPress. Left on disk, they'd
|
|
62
|
+
// silently come back to life the next time `snippet push` runs.
|
|
63
|
+
const orphans = await this.findOrphanedFiles(path, new Set(pullable.map((snippet) => snippet.id)));
|
|
58
64
|
if (this.dryRun) {
|
|
59
65
|
this.log(`[dry-run] Would pull ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to ${path}`);
|
|
66
|
+
if (orphans.length > 0) {
|
|
67
|
+
this.log(`[dry-run] Would remove ${orphans.length} local file${orphans.length === 1 ? '' : 's'} whose snippet no longer exists on WordPress: ${orphans.join(', ')}`);
|
|
68
|
+
}
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
71
|
await mkdir(path, { recursive: true });
|
|
63
|
-
const pullable = snippets.filter((snippet) => snippet.name.trim());
|
|
64
|
-
const skipped = snippets.length - pullable.length;
|
|
65
72
|
await new Listr(pullable.map((snippet) => ({
|
|
66
73
|
async task(_ctx, task) {
|
|
67
74
|
const ext = EXTENSIONS[snippet.type];
|
|
@@ -73,9 +80,36 @@ export default class Pull extends LoopressCommand {
|
|
|
73
80
|
},
|
|
74
81
|
title: `Pull ${snippet.name}`,
|
|
75
82
|
}))).run();
|
|
83
|
+
for (const file of orphans)
|
|
84
|
+
await rm(join(path, file), { force: true });
|
|
85
|
+
if (orphans.length > 0) {
|
|
86
|
+
this.warn(`Removed ${orphans.length} local file${orphans.length === 1 ? '' : 's'} whose snippet no longer exists on WordPress: ${orphans.join(', ')}`);
|
|
87
|
+
}
|
|
76
88
|
this.log(`Pulled ${pullable.length} snippet${pullable.length === 1 ? '' : 's'} to ${path}`);
|
|
77
89
|
if (skipped > 0) {
|
|
78
90
|
this.warn(`${skipped} snippet${skipped === 1 ? '' : 's'} skipped because they have no name`);
|
|
79
91
|
}
|
|
80
92
|
}
|
|
93
|
+
// Only ever matches files already following the `<id>-<slug>` convention that `snippet
|
|
94
|
+
// pull`/`push` themselves produce, so a hand-created file without a numeric prefix is
|
|
95
|
+
// never at risk of being picked up here.
|
|
96
|
+
async findOrphanedFiles(path, keepIds) {
|
|
97
|
+
let files;
|
|
98
|
+
try {
|
|
99
|
+
files = await readdir(path);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code === 'ENOENT')
|
|
103
|
+
return [];
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const knownExtensions = new Set(['json', ...Object.values(EXTENSIONS)]);
|
|
107
|
+
return files.filter((file) => {
|
|
108
|
+
const ext = extname(file).slice(1);
|
|
109
|
+
if (!knownExtensions.has(ext))
|
|
110
|
+
return false;
|
|
111
|
+
const match = /^(\d+)-/.exec(file);
|
|
112
|
+
return match !== null && !keepIds.has(Number(match[1]));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
81
115
|
}
|