@loopress/cli 0.20.1 → 0.22.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 +254 -75
- package/dist/commands/acf/list.d.ts +2 -2
- package/dist/commands/acf/list.js +4 -6
- package/dist/commands/acf/pull.js +9 -22
- package/dist/commands/acf/push.js +4 -7
- package/dist/commands/api/list.d.ts +7 -4
- package/dist/commands/api/list.js +5 -11
- package/dist/commands/api/publish.js +7 -23
- package/dist/commands/api/pull.d.ts +8 -1
- package/dist/commands/api/pull.js +22 -18
- package/dist/commands/api/push.d.ts +7 -1
- package/dist/commands/api/push.js +50 -23
- package/dist/commands/composer/init.js +6 -6
- package/dist/commands/composer/pull.d.ts +7 -1
- package/dist/commands/composer/pull.js +4 -2
- package/dist/commands/composer/push.d.ts +8 -1
- package/dist/commands/composer/push.js +3 -1
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +97 -0
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/form/list.d.ts +2 -4
- package/dist/commands/form/list.js +3 -10
- package/dist/commands/form/pull.js +15 -24
- package/dist/commands/form/push.js +17 -42
- package/dist/commands/init.js +7 -7
- package/dist/commands/login.js +9 -5
- package/dist/commands/logout.js +2 -1
- package/dist/commands/page/list.d.ts +2 -4
- package/dist/commands/page/list.js +3 -10
- package/dist/commands/page/pull.d.ts +13 -1
- package/dist/commands/page/pull.js +21 -29
- package/dist/commands/page/push.d.ts +11 -1
- package/dist/commands/page/push.js +31 -49
- package/dist/commands/plugin/add.js +3 -3
- package/dist/commands/plugin/pull.d.ts +8 -1
- package/dist/commands/plugin/pull.js +11 -11
- package/dist/commands/plugin/push.d.ts +12 -1
- package/dist/commands/plugin/push.js +41 -27
- package/dist/commands/project/config.js +12 -9
- package/dist/commands/project/list.d.ts +16 -1
- package/dist/commands/project/list.js +22 -16
- package/dist/commands/project/pull.js +3 -2
- package/dist/commands/project/push.d.ts +5 -0
- package/dist/commands/project/push.js +84 -63
- package/dist/commands/project/switch.js +2 -2
- package/dist/commands/promote.d.ts +18 -0
- package/dist/commands/promote.js +90 -0
- package/dist/commands/pull.d.ts +12 -0
- package/dist/commands/pull.js +51 -0
- package/dist/commands/push.d.ts +13 -0
- package/dist/commands/push.js +57 -0
- package/dist/commands/seo/list.d.ts +9 -2
- package/dist/commands/seo/list.js +9 -11
- package/dist/commands/seo/pull.js +14 -37
- package/dist/commands/seo/push.js +24 -48
- package/dist/commands/snippet/list.d.ts +3 -4
- package/dist/commands/snippet/list.js +5 -11
- package/dist/commands/snippet/publish.js +5 -21
- package/dist/commands/snippet/pull.d.ts +13 -1
- package/dist/commands/snippet/pull.js +18 -20
- package/dist/commands/snippet/push.d.ts +11 -1
- package/dist/commands/snippet/push.js +29 -28
- package/dist/commands/status.d.ts +13 -1
- package/dist/commands/status.js +25 -18
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +24 -0
- package/dist/config/auth.manager.d.ts +2 -2
- package/dist/config/project-config.manager.d.ts +1 -1
- package/dist/config/project-config.manager.js +5 -6
- package/dist/lib/base.d.ts +11 -2
- package/dist/lib/base.js +54 -24
- package/dist/lib/bootstrap-full-install.d.ts +1 -1
- package/dist/lib/dev-watch.d.ts +16 -0
- package/dist/lib/dev-watch.js +71 -0
- package/dist/lib/find-orphaned-files.d.ts +3 -2
- package/dist/lib/find-orphaned-files.js +40 -4
- package/dist/lib/github-release.js +1 -1
- package/dist/lib/guard-production-push.d.ts +12 -0
- package/dist/lib/guard-production-push.js +20 -0
- package/dist/lib/interactive.js +1 -1
- package/dist/lib/load-files.d.ts +1 -0
- package/dist/lib/load-files.js +40 -11
- package/dist/lib/load-snippets.d.ts +1 -1
- package/dist/lib/load-snippets.js +66 -59
- package/dist/lib/local-callback-server.js +15 -8
- package/dist/lib/plugin-detection.d.ts +1 -1
- package/dist/lib/push-command.d.ts +3 -0
- package/dist/lib/push-command.js +22 -15
- package/dist/lib/put-or-create.d.ts +13 -0
- package/dist/lib/put-or-create.js +19 -0
- package/dist/lib/readdir-tolerant.d.ts +5 -0
- package/dist/lib/readdir-tolerant.js +11 -0
- package/dist/lib/resolve-linked-project.d.ts +8 -0
- package/dist/lib/resolve-linked-project.js +27 -0
- package/dist/lib/rotate-app-password.d.ts +1 -1
- package/dist/lib/rotate-app-password.js +1 -1
- package/dist/lib/temp-admin.d.ts +3 -3
- package/dist/lib/temp-admin.js +2 -2
- package/dist/lib/validate-local.d.ts +10 -0
- package/dist/lib/validate-local.js +183 -0
- package/dist/lib/wp-authorize-flow.js +5 -1
- package/dist/lib/wp-client.d.ts +2 -2
- package/dist/lib/wp-client.js +7 -0
- package/dist/types/plugin.d.ts +4 -4
- package/dist/types/snippet.d.ts +3 -3
- package/dist/utils/composer.d.ts +2 -2
- package/dist/utils/form-format.js +1 -1
- package/dist/utils/loopress-config.d.ts +1 -1
- package/dist/utils/page-format.js +2 -2
- package/dist/utils/plugins.d.ts +5 -5
- package/dist/utils/plugins.js +3 -3
- package/dist/utils/pluralize.d.ts +1 -0
- package/dist/utils/pluralize.js +5 -0
- package/dist/utils/seo-format.d.ts +6 -6
- package/dist/utils/snippet-format.d.ts +2 -2
- package/dist/utils/snippet-format.js +10 -5
- package/oclif.manifest.json +394 -57
- package/package.json +18 -16
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, extname, join } from 'node:path';
|
|
5
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
|
-
import {
|
|
5
|
+
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
6
|
+
import { readdirTolerant } from '../../lib/readdir-tolerant.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
7
8
|
import { redirectFileBase, SEO_REDIRECTS_ENDPOINT, SEO_SETTINGS_ENDPOINT, seoPostMetaEndpoint, seoRedirectEndpoint, } from '../../utils/seo-format.js';
|
|
8
9
|
export default class Push extends PushCommand {
|
|
9
10
|
static args = {
|
|
@@ -25,7 +26,7 @@ export default class Push extends PushCommand {
|
|
|
25
26
|
await this.pushPostMeta(path);
|
|
26
27
|
await this.pushRedirects(path);
|
|
27
28
|
if (this.failedCount > 0) {
|
|
28
|
-
this.error(`${this.failedCount
|
|
29
|
+
this.error(`${pluralize(this.failedCount, 'SEO item')} failed to push.`);
|
|
29
30
|
}
|
|
30
31
|
if (this.dryRun)
|
|
31
32
|
return;
|
|
@@ -33,36 +34,20 @@ export default class Push extends PushCommand {
|
|
|
33
34
|
this.log('All SEO configuration pushed.');
|
|
34
35
|
}
|
|
35
36
|
async jsonFilesIn(dir) {
|
|
36
|
-
|
|
37
|
-
return (await readdir(dir)).filter((file) => extname(file) === '.json');
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (error.code === 'ENOENT')
|
|
41
|
-
return [];
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
37
|
+
return (await readdirTolerant(dir)).filter((file) => extname(file) === '.json');
|
|
44
38
|
}
|
|
45
39
|
async pushPostMeta(basePath) {
|
|
46
40
|
const root = join(basePath, 'post-meta');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
if (error.code === 'ENOENT')
|
|
53
|
-
return;
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
41
|
+
const postTypeDirs = (await readdirTolerant(root, { withFileTypes: true }))
|
|
42
|
+
.filter((entry) => entry.isDirectory())
|
|
43
|
+
.map((entry) => entry.name);
|
|
56
44
|
for (const postType of postTypeDirs) {
|
|
57
45
|
const dir = join(root, postType);
|
|
58
46
|
const files = await this.jsonFilesIn(dir);
|
|
59
47
|
if (files.length === 0)
|
|
60
48
|
continue;
|
|
61
|
-
this.log(`Found ${files.length
|
|
62
|
-
await
|
|
63
|
-
task: async (_ctx, task) => this.pushPostMetaFile(postType, join(dir, file), task),
|
|
64
|
-
title: `Push ${file}`,
|
|
65
|
-
})), { concurrent: false, exitOnError: false }).run();
|
|
49
|
+
this.log(`Found ${pluralize(files.length, postType + ' post-meta file')} to push`);
|
|
50
|
+
await this.runPushTasks(files, (file) => file, async (file, task) => this.pushPostMetaFile(postType, join(dir, file), task));
|
|
66
51
|
}
|
|
67
52
|
}
|
|
68
53
|
async pushPostMetaFile(postType, filePath, task) {
|
|
@@ -90,24 +75,18 @@ export default class Push extends PushCommand {
|
|
|
90
75
|
try {
|
|
91
76
|
const redirect = JSON.parse(await readFile(filePath, 'utf8'));
|
|
92
77
|
const payload = { headerCode: redirect.headerCode, sources: redirect.sources, status: redirect.status, urlTo: redirect.urlTo };
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!isNotFoundError(error))
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const created = await this.wp.post(SEO_REDIRECTS_ENDPOINT, payload);
|
|
108
|
-
await this.renameToCanonical(filePath, created);
|
|
78
|
+
// The id recorded locally may not exist on this site (e.g. a fresh install): putOrCreate
|
|
79
|
+
// falls back to POST instead of failing, adopting whatever id the site assigns.
|
|
80
|
+
const { body, created } = await putOrCreate(this.wp, {
|
|
81
|
+
id: redirect.id ?? null,
|
|
82
|
+
payload,
|
|
83
|
+
postEndpoint: SEO_REDIRECTS_ENDPOINT,
|
|
84
|
+
putEndpoint: (id) => seoRedirectEndpoint(id),
|
|
85
|
+
});
|
|
86
|
+
if (created)
|
|
87
|
+
await this.renameToCanonical(filePath, body);
|
|
109
88
|
if (task)
|
|
110
|
-
task.output = `Pushed: redirect #${created.id}`;
|
|
89
|
+
task.output = `Pushed: redirect #${created ? body.id : redirect.id}`;
|
|
111
90
|
}
|
|
112
91
|
catch (error) {
|
|
113
92
|
this.reportTaskFailure(`Failed to push ${filePath}: ${error.message}`, error, task);
|
|
@@ -118,11 +97,8 @@ export default class Push extends PushCommand {
|
|
|
118
97
|
const files = await this.jsonFilesIn(dir);
|
|
119
98
|
if (files.length === 0)
|
|
120
99
|
return;
|
|
121
|
-
this.log(`Found ${files.length
|
|
122
|
-
await
|
|
123
|
-
task: async (_ctx, task) => this.pushRedirectFile(join(dir, file), task),
|
|
124
|
-
title: `Push ${file}`,
|
|
125
|
-
})), { concurrent: false, exitOnError: false }).run();
|
|
100
|
+
this.log(`Found ${pluralize(files.length, 'redirect')} to push`);
|
|
101
|
+
await this.runPushTasks(files, (file) => file, async (file, task) => this.pushRedirectFile(join(dir, file), task));
|
|
126
102
|
}
|
|
127
103
|
async pushSettings(basePath) {
|
|
128
104
|
const file = join(basePath, 'settings.json');
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type NormalizedSnippet } from '../../utils/snippet-format.js';
|
|
2
3
|
export default class List extends LoopressCommand {
|
|
3
4
|
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
4
6
|
static examples: string[];
|
|
5
|
-
|
|
6
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
};
|
|
8
|
-
run(): Promise<void>;
|
|
7
|
+
run(): Promise<NormalizedSnippet[]>;
|
|
9
8
|
}
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
3
3
|
import { normalizeSnippet, SNIPPETS_ENDPOINT } from '../../utils/snippet-format.js';
|
|
4
4
|
export default class List extends LoopressCommand {
|
|
5
5
|
static description = 'List snippets from WordPress';
|
|
6
|
+
static enableJsonFlag = true;
|
|
6
7
|
static examples = ['$ lps snippet list'];
|
|
7
|
-
static flags = {
|
|
8
|
-
json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
|
|
9
|
-
};
|
|
10
8
|
async run() {
|
|
11
|
-
const { flags } = await this.parse(List);
|
|
12
9
|
const remoteList = await this.wp.get(SNIPPETS_ENDPOINT);
|
|
13
10
|
const snippets = remoteList.map((r) => normalizeSnippet(r));
|
|
14
|
-
if (flags.json) {
|
|
15
|
-
this.log(JSON.stringify(snippets, null, 2));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
11
|
if (snippets.length === 0) {
|
|
19
12
|
this.log('No snippets found');
|
|
20
|
-
return;
|
|
13
|
+
return snippets;
|
|
21
14
|
}
|
|
22
|
-
this.log(`Found ${snippets.length
|
|
15
|
+
this.log(`Found ${pluralize(snippets.length, 'snippet')}:`);
|
|
23
16
|
this.log('');
|
|
24
17
|
for (const snippet of snippets) {
|
|
25
18
|
this.log(` ${snippet.id}. ${snippet.name}`);
|
|
@@ -32,5 +25,6 @@ export default class List extends LoopressCommand {
|
|
|
32
25
|
}
|
|
33
26
|
this.log('');
|
|
34
27
|
}
|
|
28
|
+
return snippets;
|
|
35
29
|
}
|
|
36
30
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Args, Command } from '@oclif/core';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { authManager } from '../../config/auth.manager.js';
|
|
4
|
-
import { configManager } from '../../config/project-config.manager.js';
|
|
5
3
|
import { ApiClient } from '../../lib/api-client.js';
|
|
6
4
|
import { loadSnippets } from '../../lib/load-snippets.js';
|
|
7
|
-
import {
|
|
5
|
+
import { resolveLinkedProject } from '../../lib/resolve-linked-project.js';
|
|
6
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
7
|
import { toSlug } from '../../utils/to-slug.js';
|
|
9
8
|
// Publishes to the Loopress api (not a WordPress site), so this does not extend
|
|
10
9
|
// `LoopressCommand`/`PushCommand`: those force an environment to be resolved, but a project
|
|
@@ -18,22 +17,7 @@ export default class Publish extends Command {
|
|
|
18
17
|
static examples = ['$ lps snippet publish', '$ lps snippet publish --path ./snippets'];
|
|
19
18
|
async run() {
|
|
20
19
|
const { args } = await this.parse(Publish);
|
|
21
|
-
const 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 push\` first.`);
|
|
36
|
-
}
|
|
20
|
+
const { apiProjectId, localConfig, token } = await resolveLinkedProject((message) => this.error(message));
|
|
37
21
|
const path = args.path ?? join(localConfig.rootDir ?? '.', localConfig.snippetsDir ?? 'snippets');
|
|
38
22
|
this.log(`Publishing snippets from ${path}`);
|
|
39
23
|
let snippets;
|
|
@@ -45,14 +29,14 @@ export default class Publish extends Command {
|
|
|
45
29
|
}
|
|
46
30
|
const api = new ApiClient(token);
|
|
47
31
|
try {
|
|
48
|
-
await api.post(`projects/${
|
|
32
|
+
await api.post(`projects/${apiProjectId}/snippets/publish`, {
|
|
49
33
|
snippets: snippets.map((snippet) => this.toPayload(snippet)),
|
|
50
34
|
});
|
|
51
35
|
}
|
|
52
36
|
catch (error) {
|
|
53
37
|
this.error(error.message);
|
|
54
38
|
}
|
|
55
|
-
this.log(`Published ${snippets.length
|
|
39
|
+
this.log(`Published ${pluralize(snippets.length, 'snippet')} to your Loopress account.`);
|
|
56
40
|
}
|
|
57
41
|
toPayload(snippet) {
|
|
58
42
|
return {
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type PulledSnippet = {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
type PullResult = {
|
|
7
|
+
orphans: string[];
|
|
8
|
+
pulled: PulledSnippet[];
|
|
9
|
+
skipped: number;
|
|
10
|
+
status: 'dry-run' | 'success';
|
|
11
|
+
};
|
|
2
12
|
export default class Pull extends LoopressCommand {
|
|
3
13
|
static args: {
|
|
4
14
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
15
|
};
|
|
6
16
|
static description: string;
|
|
17
|
+
static enableJsonFlag: boolean;
|
|
7
18
|
static examples: string[];
|
|
8
19
|
static flags: {
|
|
9
20
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
21
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
22
|
};
|
|
12
|
-
run(): Promise<
|
|
23
|
+
run(): Promise<PullResult>;
|
|
13
24
|
}
|
|
25
|
+
export {};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { LoopressCommand } from '../../lib/base.js';
|
|
6
5
|
import { findOrphanedFiles, numericPrefixKey } from '../../lib/find-orphaned-files.js';
|
|
6
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
7
7
|
import { buildMetaFile, buildSnippetFile, normalizeSnippet, SNIPPETS_ENDPOINT } from '../../utils/snippet-format.js';
|
|
8
8
|
import { toSlug } from '../../utils/to-slug.js';
|
|
9
9
|
const EXTENSIONS = {
|
|
@@ -18,6 +18,7 @@ export default class Pull extends LoopressCommand {
|
|
|
18
18
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
19
19
|
};
|
|
20
20
|
static description = 'Pull snippets from WordPress';
|
|
21
|
+
static enableJsonFlag = true;
|
|
21
22
|
static examples = ['$ lps snippet pull', '$ lps snippet pull --path ./snippets'];
|
|
22
23
|
static flags = {
|
|
23
24
|
...LoopressCommand.dryRunFlag,
|
|
@@ -40,28 +41,25 @@ export default class Pull extends LoopressCommand {
|
|
|
40
41
|
extensions: ['.json', ...Object.values(EXTENSIONS).map((ext) => `.${ext}`)],
|
|
41
42
|
key: numericPrefixKey,
|
|
42
43
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await new Listr(pullable.map((snippet) => ({
|
|
52
|
-
async task(_ctx, task) {
|
|
44
|
+
const pulled = pullable.map((snippet) => ({ id: snippet.id, name: snippet.name }));
|
|
45
|
+
await this.pullDirectory(path, pullable, orphans, {
|
|
46
|
+
alwaysCreateDir: true,
|
|
47
|
+
dryRunMessage: `Would pull ${pluralize(snippets.length, 'snippet')} to ${path}`,
|
|
48
|
+
orphanReason: 'whose snippet no longer exists on WordPress',
|
|
49
|
+
pulledMessage: `Pulled ${pluralize(pullable.length, 'snippet')} to ${path}`,
|
|
50
|
+
title: (snippet) => snippet.name,
|
|
51
|
+
async write(snippet, writeDir) {
|
|
53
52
|
const ext = EXTENSIONS[snippet.type];
|
|
54
53
|
const base = `${snippet.id}-${toSlug(snippet.name)}`;
|
|
55
|
-
await writeFile(join(
|
|
56
|
-
await writeFile(join(
|
|
57
|
-
task.output = `Pulled: ${snippet.name}`;
|
|
54
|
+
await writeFile(join(writeDir, `${base}.${ext}`), buildSnippetFile(snippet));
|
|
55
|
+
await writeFile(join(writeDir, `${base}.json`), buildMetaFile(snippet));
|
|
58
56
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.log(`Pulled ${pullable.length} snippet${pullable.length === 1 ? '' : 's'} to ${path}`);
|
|
57
|
+
});
|
|
58
|
+
if (this.dryRun)
|
|
59
|
+
return { orphans, pulled, skipped, status: 'dry-run' };
|
|
63
60
|
if (skipped > 0) {
|
|
64
|
-
this.warn(`${skipped
|
|
61
|
+
this.warn(`${pluralize(skipped, 'snippet')} skipped because they have no name`);
|
|
65
62
|
}
|
|
63
|
+
return { orphans, pulled, skipped, status: 'success' };
|
|
66
64
|
}
|
|
67
65
|
}
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type PushedSnippet = {
|
|
3
|
+
id?: number;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
type PushResult = {
|
|
7
|
+
pushed: PushedSnippet[];
|
|
8
|
+
status: 'dry-run' | 'success';
|
|
9
|
+
};
|
|
2
10
|
export default class Push extends PushCommand {
|
|
3
11
|
static args: {
|
|
4
12
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
13
|
};
|
|
6
14
|
static description: string;
|
|
15
|
+
static enableJsonFlag: boolean;
|
|
7
16
|
static examples: string[];
|
|
8
17
|
static flags: {
|
|
9
18
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
19
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
20
|
};
|
|
12
|
-
run(): Promise<
|
|
21
|
+
run(): Promise<PushResult>;
|
|
13
22
|
private ensureCanonicalFilename;
|
|
14
23
|
private loadSnippets;
|
|
15
24
|
private pushSnippet;
|
|
16
25
|
private toPayload;
|
|
17
26
|
}
|
|
27
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { Listr } from 'listr2';
|
|
3
2
|
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { basename, dirname, extname, join } from 'node:path';
|
|
5
4
|
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
6
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
-
import {
|
|
6
|
+
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
8
|
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
9
9
|
import { toSlug } from '../../utils/to-slug.js';
|
|
10
10
|
export default class Push extends PushCommand {
|
|
@@ -12,6 +12,7 @@ export default class Push extends PushCommand {
|
|
|
12
12
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
13
13
|
};
|
|
14
14
|
static description = 'Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
|
|
15
|
+
static enableJsonFlag = true;
|
|
15
16
|
static examples = ['$ lps snippet push', '$ lps snippet push --path ./snippets'];
|
|
16
17
|
static flags = {
|
|
17
18
|
...PushCommand.dryRunFlag,
|
|
@@ -24,18 +25,20 @@ export default class Push extends PushCommand {
|
|
|
24
25
|
this.log(`Pushing snippets to ${url}`);
|
|
25
26
|
this.log(`Snippets path: ${path}`);
|
|
26
27
|
const snippets = await this.loadSnippets(path);
|
|
27
|
-
this.log(`Found ${snippets.length
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
this.log(`Found ${pluralize(snippets.length, 'snippet')} to push`);
|
|
29
|
+
const pushed = [];
|
|
30
|
+
await this.runPushTasks(snippets, (snippet) => snippet.name, async (snippet, task) => {
|
|
31
|
+
const id = await this.pushSnippet(snippet, task);
|
|
32
|
+
pushed.push({ id, name: snippet.name });
|
|
33
|
+
});
|
|
32
34
|
if (this.failedCount > 0) {
|
|
33
|
-
this.error(`${this.failedCount
|
|
35
|
+
this.error(`${pluralize(this.failedCount, 'snippet')} failed to push.`);
|
|
34
36
|
}
|
|
35
37
|
if (this.dryRun)
|
|
36
|
-
return;
|
|
38
|
+
return { pushed, status: 'dry-run' };
|
|
37
39
|
await this.recordSuccess();
|
|
38
40
|
this.log('All snippets pushed.');
|
|
41
|
+
return { pushed, status: 'success' };
|
|
39
42
|
}
|
|
40
43
|
// Renames the local file pair to the `<id>-<slug>` convention used by `snippet pull` whenever
|
|
41
44
|
// it doesn't already match (e.g. a hand-created `demo.php` with no id, or a stale slug after a rename).
|
|
@@ -73,7 +76,7 @@ export default class Push extends PushCommand {
|
|
|
73
76
|
}
|
|
74
77
|
async loadSnippets(path) {
|
|
75
78
|
try {
|
|
76
|
-
return await loadSnippetsFromDisk(path, (message) => this.warn(message));
|
|
79
|
+
return await loadSnippetsFromDisk(path, (message) => { this.warn(message); });
|
|
77
80
|
}
|
|
78
81
|
catch (error) {
|
|
79
82
|
this.error(error.message);
|
|
@@ -83,32 +86,30 @@ export default class Push extends PushCommand {
|
|
|
83
86
|
if (this.dryRun) {
|
|
84
87
|
if (task)
|
|
85
88
|
task.output = `[dry-run] Would push: ${snippet.name}`;
|
|
86
|
-
return;
|
|
89
|
+
return snippet.id;
|
|
87
90
|
}
|
|
88
91
|
try {
|
|
89
92
|
const payload = this.toPayload(snippet);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
93
|
+
// The id recorded locally may not exist on this site (e.g. a fresh install): putOrCreate
|
|
94
|
+
// falls back to POST instead of failing, adopting whatever id the site assigns.
|
|
95
|
+
const { body, created } = await putOrCreate(this.wp, {
|
|
96
|
+
id: snippet.id ?? null,
|
|
97
|
+
payload,
|
|
98
|
+
postEndpoint: SNIPPETS_ENDPOINT,
|
|
99
|
+
putEndpoint: (id) => `${SNIPPETS_ENDPOINT}/${id}`,
|
|
100
|
+
});
|
|
101
|
+
let { id } = snippet;
|
|
102
|
+
if (created) {
|
|
103
|
+
const createdSnippet = normalizeSnippet(body);
|
|
104
|
+
id = createdSnippet.id;
|
|
105
|
+
await this.ensureCanonicalFilename(snippet, createdSnippet.id, createdSnippet.name);
|
|
104
106
|
}
|
|
105
107
|
else {
|
|
106
|
-
|
|
107
|
-
const created = normalizeSnippet(response);
|
|
108
|
-
await this.ensureCanonicalFilename(snippet, created.id, created.name);
|
|
108
|
+
await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
|
|
109
109
|
}
|
|
110
110
|
if (task)
|
|
111
111
|
task.output = `Pushed: ${snippet.name}`;
|
|
112
|
+
return id;
|
|
112
113
|
}
|
|
113
114
|
catch (error) {
|
|
114
115
|
this.reportTaskFailure(`Failed to push ${snippet.name}: ${error.message}`, error, task);
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
type StatusInfo = {
|
|
3
|
+
environments?: string[];
|
|
4
|
+
note?: string;
|
|
5
|
+
project?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
};
|
|
8
|
+
type StatusResult = StatusInfo & {
|
|
9
|
+
configDir: string;
|
|
10
|
+
dataDir: string;
|
|
11
|
+
};
|
|
2
12
|
export default class Status extends Command {
|
|
3
13
|
static description: string;
|
|
14
|
+
static enableJsonFlag: boolean;
|
|
4
15
|
static examples: string[];
|
|
5
16
|
static flags: {
|
|
6
17
|
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
18
|
};
|
|
8
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<StatusResult>;
|
|
9
20
|
private reportActiveProject;
|
|
10
21
|
private reportEnvOverride;
|
|
11
22
|
private reportPinnedProject;
|
|
12
23
|
}
|
|
24
|
+
export {};
|
package/dist/commands/status.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readLocalConfig } from '../utils/loopress-config.js';
|
|
|
4
4
|
const c = ux.colorize;
|
|
5
5
|
export default class Status extends Command {
|
|
6
6
|
static description = 'Show which WordPress project and environment commands will target';
|
|
7
|
+
static enableJsonFlag = true;
|
|
7
8
|
static examples = ['$ lps status', '$ lps status --env staging'];
|
|
8
9
|
static flags = {
|
|
9
10
|
env: Flags.string({ description: 'Show what would be targeted with this environment, as other commands do with --env' }),
|
|
@@ -11,40 +12,41 @@ export default class Status extends Command {
|
|
|
11
12
|
async run() {
|
|
12
13
|
const { flags } = await this.parse(Status);
|
|
13
14
|
const localConfig = await readLocalConfig();
|
|
15
|
+
let info;
|
|
14
16
|
if (flags.env) {
|
|
15
|
-
this.reportEnvOverride(localConfig.projectId, flags.env);
|
|
17
|
+
info = this.reportEnvOverride(localConfig.projectId, flags.env);
|
|
16
18
|
}
|
|
17
19
|
else if (localConfig.projectId) {
|
|
18
|
-
this.reportPinnedProject(localConfig.projectId);
|
|
20
|
+
info = this.reportPinnedProject(localConfig.projectId);
|
|
19
21
|
}
|
|
20
22
|
else {
|
|
21
|
-
this.reportActiveProject();
|
|
23
|
+
info = this.reportActiveProject();
|
|
22
24
|
}
|
|
23
25
|
this.log('');
|
|
24
26
|
this.log(`Config dir: ${this.config.configDir}`);
|
|
25
27
|
this.log(`Data dir: ${this.config.dataDir}`);
|
|
28
|
+
return { ...info, configDir: this.config.configDir, dataDir: this.config.dataDir };
|
|
26
29
|
}
|
|
27
30
|
reportActiveProject() {
|
|
28
31
|
const env = configManager.getCurrentEnv();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!project) {
|
|
35
|
-
this.log('No project configured. Run `lps project config` first.');
|
|
36
|
-
return;
|
|
32
|
+
const project = env ? configManager.getCurrentProject() : undefined;
|
|
33
|
+
if (!env || !project) {
|
|
34
|
+
const note = 'No project configured. Run `lps project config` first.';
|
|
35
|
+
this.log(note);
|
|
36
|
+
return { note };
|
|
37
37
|
}
|
|
38
38
|
this.log(`Project: ${project.name} (${env.name})`);
|
|
39
39
|
this.log(`URL: ${env.url}`);
|
|
40
|
+
return { project: `${project.name} (${env.name})`, url: env.url };
|
|
40
41
|
}
|
|
41
42
|
// Mirrors base.ts:resolveEnvironment with --env: the targeted project comes from
|
|
42
43
|
// loopress.json when pinned, from the globally active project otherwise.
|
|
43
44
|
reportEnvOverride(pinnedProjectId, envName) {
|
|
44
45
|
const project = pinnedProjectId ? configManager.getProject(pinnedProjectId) : configManager.getCurrentProject();
|
|
45
46
|
if (!project) {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const note = 'No project configured. Run `lps project config` first.';
|
|
48
|
+
this.log(note);
|
|
49
|
+
return { note };
|
|
48
50
|
}
|
|
49
51
|
const env = project.environments[envName];
|
|
50
52
|
if (!env) {
|
|
@@ -52,29 +54,33 @@ export default class Status extends Command {
|
|
|
52
54
|
}
|
|
53
55
|
this.log(`Project: ${project.name} (${env.name}, via --env)`);
|
|
54
56
|
this.log(`URL: ${env.url}`);
|
|
57
|
+
return { project: `${project.name} (${env.name}, via --env)`, url: env.url };
|
|
55
58
|
}
|
|
56
59
|
reportPinnedProject(projectId) {
|
|
57
60
|
const project = configManager.getProject(projectId);
|
|
58
61
|
if (!project) {
|
|
62
|
+
const note = `loopress.json pins project "${projectId}", but it no longer exists. Run \`lps project config\` to configure it.`;
|
|
59
63
|
this.log(`loopress.json pins project "${projectId}", but it no longer exists.`);
|
|
60
64
|
this.log('Run `lps project config` to configure it.');
|
|
61
|
-
return;
|
|
65
|
+
return { note };
|
|
62
66
|
}
|
|
63
67
|
const envNames = Object.keys(project.environments);
|
|
64
68
|
if (envNames.length === 0) {
|
|
69
|
+
const note = 'No environments configured for this project. Run `lps project config` to add one.';
|
|
65
70
|
this.log(`Project: ${project.name}`);
|
|
66
|
-
this.log(
|
|
67
|
-
return;
|
|
71
|
+
this.log(note);
|
|
72
|
+
return { note, project: project.name };
|
|
68
73
|
}
|
|
69
74
|
if (envNames.length === 1) {
|
|
70
75
|
const env = project.environments[envNames[0]];
|
|
71
76
|
this.log(`Project: ${project.name} (${env.name})`);
|
|
72
77
|
this.log(`URL: ${env.url}`);
|
|
73
|
-
return;
|
|
78
|
+
return { project: `${project.name} (${env.name})`, url: env.url };
|
|
74
79
|
}
|
|
75
80
|
const current = configManager.getCurrentProject();
|
|
76
81
|
const currentEnv = current?.id === projectId ? configManager.getCurrentEnv() : null;
|
|
77
82
|
if (!currentEnv) {
|
|
83
|
+
const note = `"${project.name}" has multiple environments and isn't the globally active project. Run \`lps project switch\` to pick one.`;
|
|
78
84
|
this.log(`Project: ${project.name} ${c('yellow', '(ambiguous)')}`);
|
|
79
85
|
this.log(`Environments: ${envNames.join(', ')}`);
|
|
80
86
|
this.log('');
|
|
@@ -83,9 +89,10 @@ export default class Status extends Command {
|
|
|
83
89
|
if (current) {
|
|
84
90
|
this.log(`(Globally active project right now: "${current.name}")`);
|
|
85
91
|
}
|
|
86
|
-
return;
|
|
92
|
+
return { environments: envNames, note, project: project.name };
|
|
87
93
|
}
|
|
88
94
|
this.log(`Project: ${project.name} (${currentEnv.name})`);
|
|
89
95
|
this.log(`URL: ${currentEnv.url}`);
|
|
96
|
+
return { project: `${project.name} (${currentEnv.name})`, url: currentEnv.url };
|
|
90
97
|
}
|
|
91
98
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type ValidateResult } from '../lib/validate-local.js';
|
|
3
|
+
export default class Validate extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
run(): Promise<ValidateResult>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { validateLocal } from '../lib/validate-local.js';
|
|
3
|
+
export default class Validate extends Command {
|
|
4
|
+
static description = 'Check local tracked files are well formed and push-ready, without contacting WordPress';
|
|
5
|
+
static enableJsonFlag = true;
|
|
6
|
+
static examples = ['$ lps validate'];
|
|
7
|
+
async run() {
|
|
8
|
+
await this.parse(Validate);
|
|
9
|
+
const result = await validateLocal(process.cwd());
|
|
10
|
+
if (!this.jsonEnabled()) {
|
|
11
|
+
for (const problem of result.problems) {
|
|
12
|
+
this.log(` ✗ ${problem.file}: ${problem.message}`);
|
|
13
|
+
}
|
|
14
|
+
this.log('');
|
|
15
|
+
this.log(result.valid
|
|
16
|
+
? `${result.checked} file(s) checked, no problems found.`
|
|
17
|
+
: `${result.checked} file(s) checked, ${result.problems.length} problem(s) found.`);
|
|
18
|
+
}
|
|
19
|
+
// Non-zero exit for CI without throwing, so `--json` still prints the full report.
|
|
20
|
+
if (!result.valid)
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnvironmentConfig, LoopressConfig, ProjectConfig } from '../types/config.js';
|
|
1
|
+
import { type EnvironmentConfig, type LoopressConfig, type ProjectConfig } from '../types/config.js';
|
|
2
2
|
export declare class ProjectConfigManager {
|
|
3
3
|
private configDir?;
|
|
4
4
|
constructor(configDir?: string | undefined);
|