@loopress/cli 0.10.0 → 0.12.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 +41 -35
- package/dist/commands/init.js +24 -10
- package/dist/commands/project/config.js +1 -1
- package/dist/commands/project/remove.js +3 -2
- package/dist/commands/project/switch.d.ts +1 -2
- package/dist/commands/project/switch.js +28 -34
- package/dist/commands/project/sync.d.ts +13 -0
- package/dist/commands/project/sync.js +220 -0
- package/dist/commands/snippet/list.d.ts +0 -1
- package/dist/commands/snippet/list.js +4 -7
- package/dist/commands/snippet/pull.d.ts +2 -2
- package/dist/commands/snippet/pull.js +44 -14
- package/dist/commands/snippet/push.d.ts +1 -1
- package/dist/commands/snippet/push.js +56 -26
- package/dist/config/project-config.manager.d.ts +6 -1
- package/dist/config/project-config.manager.js +39 -3
- package/dist/help.js +1 -1
- package/dist/lib/api-client.d.ts +14 -0
- package/dist/lib/api-client.js +46 -0
- package/dist/lib/base.d.ts +0 -1
- package/dist/lib/base.js +0 -5
- package/dist/lib/push-command.js +1 -1
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +22 -1
- package/dist/types/global-config.generated.d.ts +8 -0
- package/dist/types/project-config.generated.d.ts +0 -4
- package/dist/types/snippet.d.ts +1 -1
- package/dist/utils/{snippet-plugin.d.ts → snippet-format.d.ts} +3 -19
- package/dist/utils/snippet-format.js +57 -0
- package/oclif.manifest.json +124 -139
- package/package.json +6 -1
- package/schemas/global-config.schema.json +8 -0
- package/schemas/project-config.schema.json +0 -6
- package/dist/utils/snippet-plugin-flag.d.ts +0 -3
- package/dist/utils/snippet-plugin-flag.js +0 -8
- package/dist/utils/snippet-plugin.js +0 -232
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.12.0 linux-x64 node-v24.18.0
|
|
25
25
|
$ lps --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ lps COMMAND
|
|
@@ -59,6 +59,7 @@ report.
|
|
|
59
59
|
* [`lps project list`](#lps-project-list)
|
|
60
60
|
* [`lps project remove`](#lps-project-remove)
|
|
61
61
|
* [`lps project switch`](#lps-project-switch)
|
|
62
|
+
* [`lps project sync`](#lps-project-sync)
|
|
62
63
|
* [`lps snippet list`](#lps-snippet-list)
|
|
63
64
|
* [`lps snippet pull [PATH]`](#lps-snippet-pull-path)
|
|
64
65
|
* [`lps snippet push [PATH]`](#lps-snippet-push-path)
|
|
@@ -84,7 +85,7 @@ EXAMPLES
|
|
|
84
85
|
$ lps composer pull --dry-run
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
88
|
+
_See code: [src/commands/composer/pull.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/composer/pull.ts)_
|
|
88
89
|
|
|
89
90
|
## `lps composer push`
|
|
90
91
|
|
|
@@ -106,7 +107,7 @@ EXAMPLES
|
|
|
106
107
|
$ lps composer push --dry-run
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
110
|
+
_See code: [src/commands/composer/push.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/composer/push.ts)_
|
|
110
111
|
|
|
111
112
|
## `lps help [COMMAND]`
|
|
112
113
|
|
|
@@ -143,7 +144,7 @@ EXAMPLES
|
|
|
143
144
|
$ lps init
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.
|
|
147
|
+
_See code: [src/commands/init.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/init.ts)_
|
|
147
148
|
|
|
148
149
|
## `lps login`
|
|
149
150
|
|
|
@@ -160,7 +161,7 @@ EXAMPLES
|
|
|
160
161
|
$ lps login
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.
|
|
164
|
+
_See code: [src/commands/login.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/login.ts)_
|
|
164
165
|
|
|
165
166
|
## `lps logout`
|
|
166
167
|
|
|
@@ -177,7 +178,7 @@ EXAMPLES
|
|
|
177
178
|
$ lps logout
|
|
178
179
|
```
|
|
179
180
|
|
|
180
|
-
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.
|
|
181
|
+
_See code: [src/commands/logout.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/logout.ts)_
|
|
181
182
|
|
|
182
183
|
## `lps plugin add SLUG [VERSION]`
|
|
183
184
|
|
|
@@ -205,7 +206,7 @@ EXAMPLES
|
|
|
205
206
|
$ lps plugin add contact-form-7 --dry-run
|
|
206
207
|
```
|
|
207
208
|
|
|
208
|
-
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.
|
|
209
|
+
_See code: [src/commands/plugin/add.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/plugin/add.ts)_
|
|
209
210
|
|
|
210
211
|
## `lps plugin pull`
|
|
211
212
|
|
|
@@ -227,7 +228,7 @@ EXAMPLES
|
|
|
227
228
|
$ lps plugin pull --dry-run
|
|
228
229
|
```
|
|
229
230
|
|
|
230
|
-
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
231
|
+
_See code: [src/commands/plugin/pull.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/plugin/pull.ts)_
|
|
231
232
|
|
|
232
233
|
## `lps plugin push`
|
|
233
234
|
|
|
@@ -249,7 +250,7 @@ EXAMPLES
|
|
|
249
250
|
$ lps plugin push --dry-run
|
|
250
251
|
```
|
|
251
252
|
|
|
252
|
-
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
253
|
+
_See code: [src/commands/plugin/push.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/plugin/push.ts)_
|
|
253
254
|
|
|
254
255
|
## `lps project config`
|
|
255
256
|
|
|
@@ -266,7 +267,7 @@ EXAMPLES
|
|
|
266
267
|
$ lps project config
|
|
267
268
|
```
|
|
268
269
|
|
|
269
|
-
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.
|
|
270
|
+
_See code: [src/commands/project/config.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/project/config.ts)_
|
|
270
271
|
|
|
271
272
|
## `lps project list`
|
|
272
273
|
|
|
@@ -283,7 +284,7 @@ EXAMPLES
|
|
|
283
284
|
$ lps project list
|
|
284
285
|
```
|
|
285
286
|
|
|
286
|
-
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
287
|
+
_See code: [src/commands/project/list.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/project/list.ts)_
|
|
287
288
|
|
|
288
289
|
## `lps project remove`
|
|
289
290
|
|
|
@@ -300,7 +301,7 @@ EXAMPLES
|
|
|
300
301
|
$ lps project remove
|
|
301
302
|
```
|
|
302
303
|
|
|
303
|
-
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.
|
|
304
|
+
_See code: [src/commands/project/remove.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/project/remove.ts)_
|
|
304
305
|
|
|
305
306
|
## `lps project switch`
|
|
306
307
|
|
|
@@ -317,7 +318,24 @@ EXAMPLES
|
|
|
317
318
|
$ lps project switch
|
|
318
319
|
```
|
|
319
320
|
|
|
320
|
-
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.
|
|
321
|
+
_See code: [src/commands/project/switch.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/project/switch.ts)_
|
|
322
|
+
|
|
323
|
+
## `lps project sync`
|
|
324
|
+
|
|
325
|
+
Sync locally configured projects and environments with your Loopress account
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
USAGE
|
|
329
|
+
$ lps project sync
|
|
330
|
+
|
|
331
|
+
DESCRIPTION
|
|
332
|
+
Sync locally configured projects and environments with your Loopress account
|
|
333
|
+
|
|
334
|
+
EXAMPLES
|
|
335
|
+
$ lps project sync
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
_See code: [src/commands/project/sync.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/project/sync.ts)_
|
|
321
339
|
|
|
322
340
|
## `lps snippet list`
|
|
323
341
|
|
|
@@ -325,23 +343,19 @@ List snippets from WordPress
|
|
|
325
343
|
|
|
326
344
|
```
|
|
327
345
|
USAGE
|
|
328
|
-
$ lps snippet list [-j]
|
|
346
|
+
$ lps snippet list [-j]
|
|
329
347
|
|
|
330
348
|
FLAGS
|
|
331
|
-
-j, --json
|
|
332
|
-
-p, --plugin=<option> WordPress snippet plugin to target (overrides loopress.json)
|
|
333
|
-
<options: code-snippets|wpcode>
|
|
349
|
+
-j, --json Output in JSON format
|
|
334
350
|
|
|
335
351
|
DESCRIPTION
|
|
336
352
|
List snippets from WordPress
|
|
337
353
|
|
|
338
354
|
EXAMPLES
|
|
339
355
|
$ lps snippet list
|
|
340
|
-
|
|
341
|
-
$ lps snippet list --plugin wpcode
|
|
342
356
|
```
|
|
343
357
|
|
|
344
|
-
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.
|
|
358
|
+
_See code: [src/commands/snippet/list.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/snippet/list.ts)_
|
|
345
359
|
|
|
346
360
|
## `lps snippet pull [PATH]`
|
|
347
361
|
|
|
@@ -349,15 +363,13 @@ Pull snippets from WordPress
|
|
|
349
363
|
|
|
350
364
|
```
|
|
351
365
|
USAGE
|
|
352
|
-
$ lps snippet pull [PATH] [-d]
|
|
366
|
+
$ lps snippet pull [PATH] [-d]
|
|
353
367
|
|
|
354
368
|
ARGUMENTS
|
|
355
369
|
[PATH] Path to snippets directory (overrides project config)
|
|
356
370
|
|
|
357
371
|
FLAGS
|
|
358
|
-
-d, --dry-run
|
|
359
|
-
-p, --plugin=<option> WordPress snippet plugin to target (overrides loopress.json)
|
|
360
|
-
<options: code-snippets|wpcode>
|
|
372
|
+
-d, --dry-run Show what would change without making changes
|
|
361
373
|
|
|
362
374
|
DESCRIPTION
|
|
363
375
|
Pull snippets from WordPress
|
|
@@ -366,11 +378,9 @@ EXAMPLES
|
|
|
366
378
|
$ lps snippet pull
|
|
367
379
|
|
|
368
380
|
$ lps snippet pull --path ./snippets
|
|
369
|
-
|
|
370
|
-
$ lps snippet pull --plugin wpcode
|
|
371
381
|
```
|
|
372
382
|
|
|
373
|
-
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.
|
|
383
|
+
_See code: [src/commands/snippet/pull.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/snippet/pull.ts)_
|
|
374
384
|
|
|
375
385
|
## `lps snippet push [PATH]`
|
|
376
386
|
|
|
@@ -378,15 +388,13 @@ Push snippets to WordPress. Local snippet files created or updated remotely are
|
|
|
378
388
|
|
|
379
389
|
```
|
|
380
390
|
USAGE
|
|
381
|
-
$ lps snippet push [PATH] [-d]
|
|
391
|
+
$ lps snippet push [PATH] [-d]
|
|
382
392
|
|
|
383
393
|
ARGUMENTS
|
|
384
394
|
[PATH] Path to snippets directory (overrides project config)
|
|
385
395
|
|
|
386
396
|
FLAGS
|
|
387
|
-
-d, --dry-run
|
|
388
|
-
-p, --plugin=<option> WordPress snippet plugin to target (overrides loopress.json)
|
|
389
|
-
<options: code-snippets|wpcode>
|
|
397
|
+
-d, --dry-run Show what would change without making changes
|
|
390
398
|
|
|
391
399
|
DESCRIPTION
|
|
392
400
|
Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>`
|
|
@@ -396,11 +404,9 @@ EXAMPLES
|
|
|
396
404
|
$ lps snippet push
|
|
397
405
|
|
|
398
406
|
$ lps snippet push --path ./snippets
|
|
399
|
-
|
|
400
|
-
$ lps snippet push --plugin wpcode
|
|
401
407
|
```
|
|
402
408
|
|
|
403
|
-
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.
|
|
409
|
+
_See code: [src/commands/snippet/push.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/snippet/push.ts)_
|
|
404
410
|
|
|
405
411
|
## `lps status`
|
|
406
412
|
|
|
@@ -417,5 +423,5 @@ EXAMPLES
|
|
|
417
423
|
$ lps status
|
|
418
424
|
```
|
|
419
425
|
|
|
420
|
-
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.
|
|
426
|
+
_See code: [src/commands/status.ts](https://github.com/loopress/loopress/blob/v0.12.0/src/commands/status.ts)_
|
|
421
427
|
<!-- commandsstop -->
|
package/dist/commands/init.js
CHANGED
|
@@ -3,7 +3,13 @@ import { Command } from '@oclif/core';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { configManager } from '../config/project-config.manager.js';
|
|
6
|
-
import { writeLocalConfig } from '../utils/loopress-config.js';
|
|
6
|
+
import { readLocalConfig, writeLocalConfig } from '../utils/loopress-config.js';
|
|
7
|
+
// WordPress.org slugs for the two snippet plugins the Loopress WordPress plugin supports
|
|
8
|
+
// (see SnippetModule.php, which wires up both providers and auto-detects the active one).
|
|
9
|
+
const SNIPPET_PROVIDERS = [
|
|
10
|
+
{ name: 'Code Snippets', slug: 'code-snippets' },
|
|
11
|
+
{ name: 'WPCode', slug: 'insert-headers-and-footers' },
|
|
12
|
+
];
|
|
7
13
|
export default class Init extends Command {
|
|
8
14
|
static description = 'Initialize a loopress.json config file in the current directory';
|
|
9
15
|
static examples = ['$ lps init'];
|
|
@@ -52,13 +58,6 @@ export default class Init extends Command {
|
|
|
52
58
|
});
|
|
53
59
|
projectLabel = projectId;
|
|
54
60
|
}
|
|
55
|
-
const snippetPlugin = await select({
|
|
56
|
-
choices: [
|
|
57
|
-
{ name: 'WPCode', value: 'wpcode' },
|
|
58
|
-
{ name: 'Code Snippets', value: 'code-snippets' },
|
|
59
|
-
],
|
|
60
|
-
message: 'Snippet plugin',
|
|
61
|
-
});
|
|
62
61
|
const rootDir = await input({
|
|
63
62
|
default: '.',
|
|
64
63
|
message: 'Root directory',
|
|
@@ -67,16 +66,31 @@ export default class Init extends Command {
|
|
|
67
66
|
default: 'snippets',
|
|
68
67
|
message: 'Snippets directory (relative to root)',
|
|
69
68
|
});
|
|
69
|
+
const providerChoice = await select({
|
|
70
|
+
choices: [...SNIPPET_PROVIDERS.map((p) => ({ name: p.name, value: p.slug })), { name: 'None / already installed', value: '__none__' }],
|
|
71
|
+
message: 'Snippet provider',
|
|
72
|
+
});
|
|
70
73
|
const config = {
|
|
71
74
|
projectId,
|
|
72
75
|
rootDir,
|
|
73
|
-
snippetPlugin: snippetPlugin,
|
|
74
76
|
snippetsDir,
|
|
75
77
|
};
|
|
76
78
|
await writeLocalConfig(config);
|
|
79
|
+
let providerVersion;
|
|
80
|
+
if (providerChoice !== '__none__') {
|
|
81
|
+
try {
|
|
82
|
+
await this.config.runCommand('plugin:add', [providerChoice]);
|
|
83
|
+
providerVersion = (await readLocalConfig()).plugins?.[providerChoice];
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.warn(error.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
this.log(`\n✓ loopress.json created`);
|
|
78
90
|
this.log(` Project: ${projectLabel}`);
|
|
79
|
-
this.log(` Plugin: ${snippetPlugin}`);
|
|
80
91
|
this.log(` Snippets: ${join(rootDir, snippetsDir)}`);
|
|
92
|
+
if (providerChoice !== '__none__' && providerVersion) {
|
|
93
|
+
this.log(` Plugin: ${providerChoice}@${providerVersion}`);
|
|
94
|
+
}
|
|
81
95
|
}
|
|
82
96
|
}
|
|
@@ -106,6 +106,6 @@ export default class Config extends Command {
|
|
|
106
106
|
return true;
|
|
107
107
|
},
|
|
108
108
|
});
|
|
109
|
-
return { projectId: configManager.createProjectId(), projectName: projectName.trim() };
|
|
109
|
+
return { projectId: configManager.createProjectId(projectName.trim()), projectName: projectName.trim() };
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -17,13 +17,14 @@ export default class Remove extends Command {
|
|
|
17
17
|
const currentMarker = project.isCurrent ? ' [current]' : '';
|
|
18
18
|
targets.push({ kind: 'project', projectId: project.id, projectName: project.name });
|
|
19
19
|
const projectChoice = {
|
|
20
|
-
|
|
20
|
+
description: 'Also removes all its environments below.',
|
|
21
|
+
name: `${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
|
|
21
22
|
value: String(targets.length - 1),
|
|
22
23
|
};
|
|
23
24
|
const envChoices = configManager.listEnvironments(project.id).map((env) => {
|
|
24
25
|
targets.push({ env: env.name, kind: 'env', projectId: project.id, projectName: project.name });
|
|
25
26
|
return {
|
|
26
|
-
name: ` ${env.
|
|
27
|
+
name: ` ${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
27
28
|
value: String(targets.length - 1),
|
|
28
29
|
};
|
|
29
30
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts';
|
|
2
|
-
import { Command } from '@oclif/core';
|
|
1
|
+
import { select, Separator } from '@inquirer/prompts';
|
|
2
|
+
import { Command, ux } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
+
const c = ux.colorize;
|
|
4
5
|
export default class Switch extends Command {
|
|
5
6
|
static description = 'Switch the active project and environment';
|
|
6
7
|
static examples = ['$ lps project switch'];
|
|
@@ -10,44 +11,37 @@ export default class Switch extends Command {
|
|
|
10
11
|
if (projects.length === 0) {
|
|
11
12
|
this.error('No projects configured. Run `lps project config` first.');
|
|
12
13
|
}
|
|
13
|
-
const {
|
|
14
|
-
const envName = await this.resolveEnvironment(projectId, projectName);
|
|
14
|
+
const { envName, projectId, projectName } = await this.resolveSelection(projects);
|
|
15
15
|
configManager.setCurrent(projectId, envName);
|
|
16
16
|
this.log(`✓ Switched to "${projectName}/${envName}"`);
|
|
17
17
|
}
|
|
18
|
-
async
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
async resolveSelection(projects) {
|
|
19
|
+
const groups = projects
|
|
20
|
+
.map((project) => ({ envs: configManager.listEnvironments(project.id), project }))
|
|
21
|
+
.filter(({ envs }) => envs.length > 0);
|
|
22
|
+
if (groups.length === 0) {
|
|
23
|
+
this.error('No environments configured. Run `lps project config` first.');
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const entries = groups.flatMap(({ envs, project }) => envs.map((env) => ({ env, project })));
|
|
26
|
+
if (entries.length === 1) {
|
|
27
|
+
const { env, project } = entries[0];
|
|
28
|
+
return { envName: env.name, projectId: project.id, projectName: project.name };
|
|
29
|
+
}
|
|
30
|
+
const choices = groups.flatMap(({ envs, project }) => [
|
|
31
|
+
new Separator(project.isCurrent ? c('green', `─── ${project.name} ───`) : c('dim', `─── ${project.name} ───`)),
|
|
32
|
+
...envs.map((env) => ({
|
|
33
|
+
name: `${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
34
|
+
value: `${project.id}::${env.name}`,
|
|
29
35
|
})),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
async resolveProject(projects) {
|
|
35
|
-
if (projects.length === 1)
|
|
36
|
-
return { id: projects[0].id, name: projects[0].name };
|
|
36
|
+
]);
|
|
37
|
+
const current = entries.find(({ env }) => env.isCurrent);
|
|
37
38
|
const chosen = await select({
|
|
38
|
-
choices
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const currentMarker = project.isCurrent ? ' [current]' : '';
|
|
42
|
-
return {
|
|
43
|
-
name: `${project.isCurrent ? '●' : '○'} ${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
|
|
44
|
-
value: project.id,
|
|
45
|
-
};
|
|
46
|
-
}),
|
|
47
|
-
default: projects.find((project) => project.isCurrent)?.id,
|
|
48
|
-
message: 'Select active project',
|
|
39
|
+
choices,
|
|
40
|
+
default: current && `${current.project.id}::${current.env.name}`,
|
|
41
|
+
message: 'Select project / environment',
|
|
49
42
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
43
|
+
const [projectId, envName] = chosen.split('::');
|
|
44
|
+
const { project } = groups.find((group) => group.project.id === projectId);
|
|
45
|
+
return { envName, projectId, projectName: project.name };
|
|
52
46
|
}
|
|
53
47
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Sync extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
run(): Promise<void>;
|
|
6
|
+
private applyEnvironment;
|
|
7
|
+
private applyProject;
|
|
8
|
+
private fetchApiProjects;
|
|
9
|
+
private planEnvironment;
|
|
10
|
+
private planProject;
|
|
11
|
+
private pullProject;
|
|
12
|
+
private pushCredentials;
|
|
13
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Command } from '@oclif/core';
|
|
3
|
+
import { Listr } from 'listr2';
|
|
4
|
+
import slugify from 'slugify';
|
|
5
|
+
import { authManager } from '../../config/auth.manager.js';
|
|
6
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
7
|
+
import { ApiClient } from '../../lib/api-client.js';
|
|
8
|
+
export default class Sync extends Command {
|
|
9
|
+
static description = 'Sync locally configured projects and environments with your Loopress account';
|
|
10
|
+
static examples = ['$ lps project sync'];
|
|
11
|
+
async run() {
|
|
12
|
+
await this.parse(Sync);
|
|
13
|
+
const token = authManager.getAuth()?.token;
|
|
14
|
+
if (!token) {
|
|
15
|
+
this.error('Not logged in. Run `lps login` first.');
|
|
16
|
+
}
|
|
17
|
+
const projects = configManager.listProjects();
|
|
18
|
+
if (projects.length === 0) {
|
|
19
|
+
this.log('No projects configured. Run `lps project config` first.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const api = new ApiClient(token);
|
|
23
|
+
const apiProjects = await this.fetchApiProjects(api);
|
|
24
|
+
const claimedProjectIds = new Set();
|
|
25
|
+
const envPlansByProject = new Map();
|
|
26
|
+
const projectPlans = [];
|
|
27
|
+
for (const project of projects) {
|
|
28
|
+
const plan = await this.planProject(project, apiProjects, claimedProjectIds);
|
|
29
|
+
projectPlans.push(plan);
|
|
30
|
+
const apiProject = plan.apiProjectId ? apiProjects.find((candidate) => candidate.id === plan.apiProjectId) : undefined;
|
|
31
|
+
const claimedEnvironmentIds = new Set();
|
|
32
|
+
const envPlans = [];
|
|
33
|
+
for (const env of configManager.listEnvironments(project.id)) {
|
|
34
|
+
envPlans.push(plan.action === 'create'
|
|
35
|
+
? { action: 'create', env, projectId: project.id }
|
|
36
|
+
: await this.planEnvironment(env, project.id, apiProject, claimedEnvironmentIds));
|
|
37
|
+
}
|
|
38
|
+
envPlansByProject.set(project.id, envPlans);
|
|
39
|
+
}
|
|
40
|
+
let projectCount = projectPlans.filter((plan) => plan.action === 'synced').length;
|
|
41
|
+
let environmentCount = [...envPlansByProject.values()].flat().filter((plan) => plan.action === 'synced').length;
|
|
42
|
+
const projectsNeedingWork = projectPlans.filter((plan) => plan.action !== 'synced');
|
|
43
|
+
if (projectsNeedingWork.length > 0) {
|
|
44
|
+
await new Listr(projectsNeedingWork.map((plan) => ({
|
|
45
|
+
task: async (_ctx, task) => {
|
|
46
|
+
await this.applyProject(api, plan, task);
|
|
47
|
+
projectCount++;
|
|
48
|
+
},
|
|
49
|
+
title: plan.action === 'create'
|
|
50
|
+
? `Create project "${plan.project.name}" on the API`
|
|
51
|
+
: `Link project "${plan.project.name}" to the API`,
|
|
52
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
53
|
+
}
|
|
54
|
+
const envsNeedingWork = projectPlans
|
|
55
|
+
.filter((plan) => Boolean(plan.apiProjectId))
|
|
56
|
+
.flatMap((plan) => (envPlansByProject.get(plan.project.id) ?? [])
|
|
57
|
+
.filter((envPlan) => envPlan.action !== 'synced')
|
|
58
|
+
.map((envPlan) => ({ apiProjectId: plan.apiProjectId, envPlan, projectName: plan.project.name })));
|
|
59
|
+
if (envsNeedingWork.length > 0) {
|
|
60
|
+
await new Listr(envsNeedingWork.map(({ apiProjectId, envPlan, projectName }) => ({
|
|
61
|
+
task: async (_ctx, task) => {
|
|
62
|
+
await this.applyEnvironment(api, apiProjectId, envPlan, task);
|
|
63
|
+
environmentCount++;
|
|
64
|
+
},
|
|
65
|
+
title: envPlan.action === 'create'
|
|
66
|
+
? `Create environment "${envPlan.env.name}" on "${projectName}"`
|
|
67
|
+
: `Link environment "${envPlan.env.name}" on "${projectName}"`,
|
|
68
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
69
|
+
}
|
|
70
|
+
for (const plan of projectPlans) {
|
|
71
|
+
if (!plan.apiProjectId)
|
|
72
|
+
continue;
|
|
73
|
+
for (const envPlan of envPlansByProject.get(plan.project.id) ?? []) {
|
|
74
|
+
if (!envPlan.apiEnvironmentId || !envPlan.env.token)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
await this.pushCredentials(api, plan.apiProjectId, envPlan.apiEnvironmentId, envPlan.env);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.warn(`Failed to sync "${plan.project.name}/${envPlan.env.name}": ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const newApiProjects = apiProjects.filter((candidate) => !claimedProjectIds.has(candidate.id));
|
|
85
|
+
if (newApiProjects.length > 0) {
|
|
86
|
+
await new Listr(newApiProjects.map((apiProject) => ({
|
|
87
|
+
task: async (_ctx, task) => {
|
|
88
|
+
this.pullProject(apiProject, task);
|
|
89
|
+
projectCount++;
|
|
90
|
+
environmentCount += apiProject.environments.length;
|
|
91
|
+
},
|
|
92
|
+
title: `Pull project "${apiProject.name}" from the API`,
|
|
93
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
94
|
+
}
|
|
95
|
+
this.log(`\n✓ Synced ${projectCount} project${projectCount === 1 ? '' : 's'}, ${environmentCount} environment${environmentCount === 1 ? '' : 's'} with your Loopress account`);
|
|
96
|
+
}
|
|
97
|
+
async applyEnvironment(api, apiProjectId, envPlan, task) {
|
|
98
|
+
try {
|
|
99
|
+
if (envPlan.action === 'create') {
|
|
100
|
+
const created = await api.post(`projects/${apiProjectId}/environments`, {
|
|
101
|
+
name: envPlan.env.name,
|
|
102
|
+
url: envPlan.env.url,
|
|
103
|
+
});
|
|
104
|
+
envPlan.apiEnvironmentId = created.id;
|
|
105
|
+
}
|
|
106
|
+
if (!envPlan.apiEnvironmentId) {
|
|
107
|
+
throw new Error(`No API environment id resolved for "${envPlan.env.name}" (action: ${envPlan.action})`);
|
|
108
|
+
}
|
|
109
|
+
configManager.setEnvironmentApiId(envPlan.projectId, envPlan.env.name, envPlan.apiEnvironmentId);
|
|
110
|
+
if (task)
|
|
111
|
+
task.output = envPlan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const message = `Failed to sync "${envPlan.env.name}": ${error.message}`;
|
|
115
|
+
if (task)
|
|
116
|
+
task.output = message;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async applyProject(api, plan, task) {
|
|
121
|
+
try {
|
|
122
|
+
if (plan.action === 'create') {
|
|
123
|
+
const created = await api.post('projects', { name: plan.project.name });
|
|
124
|
+
plan.apiProjectId = created.id;
|
|
125
|
+
}
|
|
126
|
+
if (!plan.apiProjectId) {
|
|
127
|
+
throw new Error(`No API project id resolved for "${plan.project.name}" (action: ${plan.action})`);
|
|
128
|
+
}
|
|
129
|
+
configManager.setProjectApiId(plan.project.id, plan.apiProjectId);
|
|
130
|
+
if (task)
|
|
131
|
+
task.output = plan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = `Failed to sync project "${plan.project.name}": ${error.message}`;
|
|
135
|
+
if (task)
|
|
136
|
+
task.output = message;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async fetchApiProjects(api) {
|
|
141
|
+
try {
|
|
142
|
+
return await api.get('projects');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.warn(`Could not fetch existing projects from the API, will create everything as new: ${error.message}`);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async planEnvironment(env, projectId, apiProject, claimedEnvironmentIds) {
|
|
150
|
+
if (env.apiEnvironmentId) {
|
|
151
|
+
claimedEnvironmentIds.add(env.apiEnvironmentId);
|
|
152
|
+
return { action: 'synced', apiEnvironmentId: env.apiEnvironmentId, env, projectId };
|
|
153
|
+
}
|
|
154
|
+
const match = apiProject?.environments.find((candidate) => candidate.name === env.name && !claimedEnvironmentIds.has(candidate.id));
|
|
155
|
+
if (match) {
|
|
156
|
+
const link = await confirm({
|
|
157
|
+
default: true,
|
|
158
|
+
message: `Environment "${env.name}" already exists on "${apiProject?.name}". Link to it instead of creating a new one?`,
|
|
159
|
+
});
|
|
160
|
+
if (link) {
|
|
161
|
+
claimedEnvironmentIds.add(match.id);
|
|
162
|
+
return { action: 'link', apiEnvironmentId: match.id, env, projectId };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { action: 'create', env, projectId };
|
|
166
|
+
}
|
|
167
|
+
async planProject(project, apiProjects, claimedProjectIds) {
|
|
168
|
+
if (project.apiProjectId) {
|
|
169
|
+
claimedProjectIds.add(project.apiProjectId);
|
|
170
|
+
return { action: 'synced', apiProjectId: project.apiProjectId, project };
|
|
171
|
+
}
|
|
172
|
+
const slug = slugify(project.name, { lower: true, strict: true });
|
|
173
|
+
const match = apiProjects.find((candidate) => candidate.slug === slug && !claimedProjectIds.has(candidate.id));
|
|
174
|
+
if (match) {
|
|
175
|
+
const link = await confirm({
|
|
176
|
+
default: true,
|
|
177
|
+
message: `A project named "${project.name}" already exists on your account. Link to it instead of creating a new one?`,
|
|
178
|
+
});
|
|
179
|
+
if (link) {
|
|
180
|
+
claimedProjectIds.add(match.id);
|
|
181
|
+
return { action: 'link', apiProjectId: match.id, project };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { action: 'create', project };
|
|
185
|
+
}
|
|
186
|
+
pullProject(apiProject, task) {
|
|
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 };
|
|
192
|
+
for (const env of apiProject.environments) {
|
|
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,
|
|
203
|
+
apiProjectId: apiProject.id,
|
|
204
|
+
environments,
|
|
205
|
+
name: apiProject.name,
|
|
206
|
+
});
|
|
207
|
+
const envCount = apiProject.environments.length;
|
|
208
|
+
if (task)
|
|
209
|
+
task.output = `Pulled with ${envCount} environment${envCount === 1 ? '' : 's'}`;
|
|
210
|
+
}
|
|
211
|
+
async pushCredentials(api, apiProjectId, apiEnvironmentId, env) {
|
|
212
|
+
if (!env.token)
|
|
213
|
+
return;
|
|
214
|
+
const [username, ...rest] = env.token.split(':');
|
|
215
|
+
await api.put(`projects/${apiProjectId}/environments/${apiEnvironmentId}/credentials`, {
|
|
216
|
+
password: rest.join(':'),
|
|
217
|
+
username,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -3,7 +3,6 @@ export default class List extends LoopressCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
6
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
7
|
};
|
|
9
8
|
run(): Promise<void>;
|