@loopress/cli 0.21.0 → 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 +169 -56
- package/dist/commands/acf/list.d.ts +2 -2
- package/dist/commands/acf/list.js +2 -5
- package/dist/commands/acf/pull.js +9 -22
- package/dist/commands/acf/push.js +3 -6
- package/dist/commands/api/list.js +2 -1
- package/dist/commands/api/publish.js +6 -22
- package/dist/commands/api/pull.js +12 -17
- package/dist/commands/api/push.js +7 -10
- package/dist/commands/dev.js +1 -1
- package/dist/commands/doctor.js +2 -1
- 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/login.js +6 -2
- package/dist/commands/logout.js +2 -1
- package/dist/commands/page/pull.js +17 -28
- package/dist/commands/page/push.js +27 -52
- package/dist/commands/plugin/pull.d.ts +1 -0
- package/dist/commands/plugin/pull.js +8 -10
- package/dist/commands/plugin/push.d.ts +3 -0
- package/dist/commands/plugin/push.js +32 -22
- package/dist/commands/project/config.js +4 -1
- 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 +79 -58
- 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.js +2 -1
- package/dist/commands/snippet/publish.js +5 -21
- package/dist/commands/snippet/pull.js +15 -20
- package/dist/commands/snippet/push.js +21 -30
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +24 -0
- package/dist/lib/base.d.ts +9 -0
- package/dist/lib/base.js +51 -22
- package/dist/lib/guard-production-push.d.ts +12 -0
- package/dist/lib/guard-production-push.js +20 -0
- package/dist/lib/load-snippets.js +65 -60
- package/dist/lib/local-callback-server.js +9 -4
- 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/validate-local.d.ts +10 -0
- package/dist/lib/validate-local.js +183 -0
- package/dist/utils/pluralize.d.ts +1 -0
- package/dist/utils/pluralize.js +5 -0
- package/oclif.manifest.json +242 -27
- package/package.json +16 -15
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
2
3
|
export default class List extends LoopressCommand {
|
|
3
4
|
static description = 'List custom API route files from WordPress';
|
|
4
5
|
static enableJsonFlag = true;
|
|
@@ -9,7 +10,7 @@ export default class List extends LoopressCommand {
|
|
|
9
10
|
this.log('No API route files found');
|
|
10
11
|
return files;
|
|
11
12
|
}
|
|
12
|
-
this.log(`Found ${files.length
|
|
13
|
+
this.log(`Found ${pluralize(files.length, 'route file')}:`);
|
|
13
14
|
this.log('');
|
|
14
15
|
for (const file of files) {
|
|
15
16
|
this.log(` ${file.filename}`);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Args, Command } from '@oclif/core';
|
|
2
2
|
import { basename, 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 { loadFiles } from '../../lib/load-files.js';
|
|
7
|
-
import {
|
|
5
|
+
import { resolveLinkedProject } from '../../lib/resolve-linked-project.js';
|
|
6
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
7
|
// Publishes to the Loopress api (not a WordPress site), so this does not extend
|
|
9
8
|
// `LoopressCommand`/`PushCommand`: those force an environment to be resolved, but publishing
|
|
10
9
|
// for cross-project sharing doesn't need one, same reasoning as `snippet publish`. An API route
|
|
@@ -20,22 +19,7 @@ export default class Publish extends Command {
|
|
|
20
19
|
static examples = ['$ lps api publish', '$ lps api publish --path ./api'];
|
|
21
20
|
async run() {
|
|
22
21
|
const { args } = await this.parse(Publish);
|
|
23
|
-
const token =
|
|
24
|
-
if (!token) {
|
|
25
|
-
this.error('Not logged in. Run `lps login` first.');
|
|
26
|
-
}
|
|
27
|
-
const localConfig = await readLocalConfig();
|
|
28
|
-
const projectId = localConfig.projectId ?? configManager.getCurrentProject()?.id;
|
|
29
|
-
if (!projectId) {
|
|
30
|
-
this.error('No project configured. Run `lps project config` first.');
|
|
31
|
-
}
|
|
32
|
-
const project = configManager.getProject(projectId);
|
|
33
|
-
if (!project) {
|
|
34
|
-
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
35
|
-
}
|
|
36
|
-
if (!project.apiProjectId) {
|
|
37
|
-
this.error(`Project "${project.name}" is not linked to your Loopress account yet. Run \`lps project push\` first.`);
|
|
38
|
-
}
|
|
22
|
+
const { apiProjectId, localConfig, token } = await resolveLinkedProject((message) => this.error(message));
|
|
39
23
|
const path = args.path ?? join(localConfig.rootDir ?? '.', localConfig.apiDir ?? 'api');
|
|
40
24
|
this.log(`Publishing API routes from ${path}`);
|
|
41
25
|
const files = await loadFiles(path, {
|
|
@@ -45,16 +29,16 @@ 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}/api-routes/publish/upsert`, {
|
|
49
33
|
routes: files,
|
|
50
34
|
});
|
|
51
|
-
await api.post(`projects/${
|
|
35
|
+
await api.post(`projects/${apiProjectId}/api-routes/publish/prune`, {
|
|
52
36
|
filenames: files.map((file) => file.filename),
|
|
53
37
|
});
|
|
54
38
|
}
|
|
55
39
|
catch (error) {
|
|
56
40
|
this.error(error.message);
|
|
57
41
|
}
|
|
58
|
-
this.log(`Published ${files.length
|
|
42
|
+
this.log(`Published ${pluralize(files.length, 'route')} to your Loopress account.`);
|
|
59
43
|
}
|
|
60
44
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { Listr } from 'listr2';
|
|
3
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
4
|
import { LoopressCommand } from '../../lib/base.js';
|
|
6
5
|
import { basenameKey, findOrphanedFiles } from '../../lib/find-orphaned-files.js';
|
|
6
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
7
7
|
export default class Pull extends LoopressCommand {
|
|
8
8
|
static args = {
|
|
9
9
|
path: Args.string({ description: 'Path to api directory (overrides project config)' }),
|
|
@@ -31,28 +31,23 @@ export default class Pull extends LoopressCommand {
|
|
|
31
31
|
recursive: true,
|
|
32
32
|
});
|
|
33
33
|
const pulled = files.map((file) => file.filename);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await new Listr(files.map((file) => ({
|
|
43
|
-
async task(_ctx, task) {
|
|
44
|
-
const filePath = join(path, `${file.filename}.php`);
|
|
34
|
+
await this.pullDirectory(path, files, orphans, {
|
|
35
|
+
alwaysCreateDir: true,
|
|
36
|
+
dryRunMessage: `Would pull ${pluralize(files.length, 'route file')} to ${path}`,
|
|
37
|
+
orphanReason: 'whose route no longer exists on WordPress',
|
|
38
|
+
pulledMessage: `Pulled ${pluralize(files.length, 'route file')} to ${path}`,
|
|
39
|
+
title: (file) => file.filename,
|
|
40
|
+
async write(file, writeDir) {
|
|
41
|
+
const filePath = join(writeDir, `${file.filename}.php`);
|
|
45
42
|
// filename can contain '/' (a path-param route, e.g. invoice-pdf/[order_id]):
|
|
46
43
|
// writeFile() doesn't create parent directories on its own the way mkdir()'s
|
|
47
44
|
// recursive option does above for the top-level path.
|
|
48
45
|
await mkdir(dirname(filePath), { recursive: true });
|
|
49
46
|
await writeFile(filePath, file.content);
|
|
50
|
-
task.output = `Pulled: ${file.filename}`;
|
|
51
47
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.log(`Pulled ${files.length} route file${files.length === 1 ? '' : 's'} to ${path}`);
|
|
48
|
+
});
|
|
49
|
+
if (this.dryRun)
|
|
50
|
+
return { orphans, pulled, status: 'dry-run' };
|
|
56
51
|
return { orphans, pulled, status: 'success' };
|
|
57
52
|
}
|
|
58
53
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { Listr } from 'listr2';
|
|
3
2
|
import { relative, sep } from 'node:path';
|
|
4
3
|
import { authManager } from '../../config/auth.manager.js';
|
|
5
4
|
import { ApiClient } from '../../lib/api-client.js';
|
|
6
5
|
import { loadFiles as loadDirectoryFiles } from '../../lib/load-files.js';
|
|
7
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
8
|
// Mirrors the server's own allowlist (wordpress-plugin ApiFilesController::isValidFilename()):
|
|
9
9
|
// filename is sent as a body param (not a URL path segment: a nested path can contain '/' and
|
|
10
10
|
// '[]', and depending on a server to handle a percent-encoded slash in a URL path correctly is
|
|
@@ -36,17 +36,14 @@ export default class Push extends PushCommand {
|
|
|
36
36
|
this.log(`Pushing API routes to ${url}`);
|
|
37
37
|
this.log(`API path: ${path}`);
|
|
38
38
|
const files = await this.loadFiles(path);
|
|
39
|
-
this.log(`Found ${files.length
|
|
39
|
+
this.log(`Found ${pluralize(files.length, 'route file')} to push`);
|
|
40
40
|
const pushed = [];
|
|
41
|
-
await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
title: `Push ${file.filename}`,
|
|
47
|
-
})), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
41
|
+
await this.runPushTasks(files, (file) => file.filename, async (file, task) => {
|
|
42
|
+
await this.pushFile(file, task);
|
|
43
|
+
pushed.push(file.filename);
|
|
44
|
+
});
|
|
48
45
|
if (this.failedCount > 0) {
|
|
49
|
-
this.error(`${this.failedCount
|
|
46
|
+
this.error(`${pluralize(this.failedCount, 'route file')} failed to push.`);
|
|
50
47
|
}
|
|
51
48
|
if (this.dryRun)
|
|
52
49
|
return { pushed, status: 'dry-run' };
|
package/dist/commands/dev.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildWatchTargets, createDebouncedBatcher, resolveResourceTypes, RESOUR
|
|
|
6
6
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
7
7
|
const DEBOUNCE_MS = 400;
|
|
8
8
|
// Noise no one wants pushed: VCS internals, deps, and editor/OS droppings.
|
|
9
|
-
const IGNORED = /(^|[/\\])(\.git|node_modules|\.DS_Store)(?:[/\\]|$)
|
|
9
|
+
const IGNORED = /(?:(^|[/\\])(\.git|node_modules|\.DS_Store)(?:[/\\]|$))|(?:\.swp$)/;
|
|
10
10
|
export default class Dev extends Command {
|
|
11
11
|
static description = 'Watch project files and push changes to the local WordPress instance as they happen. Always targets the "local" environment, run `lps snippet push` etc. directly for any other environment.';
|
|
12
12
|
static examples = ['$ lps dev', '$ lps dev --only=snippets,pages', '$ lps dev --skip=plugins'];
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { configManager } from '../config/project-config.manager.js';
|
|
|
2
2
|
import { LoopressCommand } from '../lib/base.js';
|
|
3
3
|
import { isNotFoundError } from '../lib/wp-client.js';
|
|
4
4
|
import { diagnoseWpSite } from '../lib/wp-site-diagnostic.js';
|
|
5
|
+
import { pluralize } from '../utils/pluralize.js';
|
|
5
6
|
export default class Doctor extends LoopressCommand {
|
|
6
7
|
static description = 'Diagnose connectivity, plugin and credential problems for the targeted environment';
|
|
7
8
|
static examples = ['$ lps doctor', '$ lps doctor --env production'];
|
|
@@ -45,7 +46,7 @@ export default class Doctor extends LoopressCommand {
|
|
|
45
46
|
await this.reportPluginVersion();
|
|
46
47
|
this.log('');
|
|
47
48
|
if (failed > 0) {
|
|
48
|
-
this.error(`${failed
|
|
49
|
+
this.error(`${pluralize(failed, 'check')} failed.`, { exit: 1 });
|
|
49
50
|
}
|
|
50
51
|
this.log('All checks passed.');
|
|
51
52
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
2
|
export default class List extends LoopressCommand {
|
|
3
3
|
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
4
5
|
static examples: string[];
|
|
5
|
-
|
|
6
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
};
|
|
8
|
-
run(): Promise<void>;
|
|
6
|
+
run(): Promise<Array<Record<string, unknown>>>;
|
|
9
7
|
}
|
|
@@ -1,26 +1,19 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
2
|
import { FORM_ENDPOINT, getFormId, getFormTitle } from '../../utils/form-format.js';
|
|
4
3
|
export default class List extends LoopressCommand {
|
|
5
4
|
static description = 'List forms from WordPress';
|
|
5
|
+
static enableJsonFlag = true;
|
|
6
6
|
static examples = ['$ lps form list'];
|
|
7
|
-
static flags = {
|
|
8
|
-
json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
|
|
9
|
-
};
|
|
10
7
|
async run() {
|
|
11
|
-
const { flags } = await this.parse(List);
|
|
12
8
|
const forms = await this.wp.get(FORM_ENDPOINT);
|
|
13
|
-
if (flags.json) {
|
|
14
|
-
this.log(JSON.stringify(forms, null, 2));
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
9
|
this.log(`Forms (${forms.length}):`);
|
|
18
10
|
if (forms.length === 0) {
|
|
19
11
|
this.log(' (none)');
|
|
20
|
-
return;
|
|
12
|
+
return forms;
|
|
21
13
|
}
|
|
22
14
|
for (const form of forms) {
|
|
23
15
|
this.log(` ${getFormId(form) ?? '(no id)'}. ${getFormTitle(form)}`);
|
|
24
16
|
}
|
|
17
|
+
return forms;
|
|
25
18
|
}
|
|
26
19
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
7
6
|
import { FORM_ENDPOINT, getFormId, getFormTitle } from '../../utils/form-format.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
8
|
import { toSlug } from '../../utils/to-slug.js';
|
|
9
9
|
export default class Pull extends LoopressCommand {
|
|
10
10
|
static args = {
|
|
@@ -31,30 +31,21 @@ export default class Pull extends LoopressCommand {
|
|
|
31
31
|
extensions: ['.json'],
|
|
32
32
|
key: numericPrefixKey,
|
|
33
33
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
await this.pullDirectory(path, withId, orphans, {
|
|
35
|
+
dryRunMessage: `Would pull ${pluralize(withId.length, 'form')} to ${path}`,
|
|
36
|
+
orphanReason: `in ${path} no longer present on WordPress`,
|
|
37
|
+
pulledMessage: `Pulled ${pluralize(withId.length, 'form')} to ${path}`,
|
|
38
|
+
title: (form) => getFormTitle(form),
|
|
39
|
+
async write(form, writeDir) {
|
|
40
|
+
const id = getFormId(form);
|
|
41
|
+
const title = getFormTitle(form);
|
|
42
|
+
await writeFile(join(writeDir, `${id}-${toSlug(title, 'untitled')}.json`), JSON.stringify(form, null, 2) + '\n');
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (this.dryRun)
|
|
39
46
|
return;
|
|
40
|
-
}
|
|
41
|
-
if (withId.length > 0)
|
|
42
|
-
await mkdir(path, { recursive: true });
|
|
43
|
-
await new Listr(withId.map((form) => {
|
|
44
|
-
const id = getFormId(form);
|
|
45
|
-
const title = getFormTitle(form);
|
|
46
|
-
return {
|
|
47
|
-
async task(_ctx, task) {
|
|
48
|
-
await writeFile(join(path, `${id}-${toSlug(title, 'untitled')}.json`), JSON.stringify(form, null, 2) + '\n');
|
|
49
|
-
task.output = `Pulled: ${title}`;
|
|
50
|
-
},
|
|
51
|
-
title: `Pull ${title}`,
|
|
52
|
-
};
|
|
53
|
-
})).run();
|
|
54
|
-
await this.removeOrphanedFiles(path, orphans, `in ${path} no longer present on WordPress`);
|
|
55
|
-
this.log(`Pulled ${withId.length} form${withId.length === 1 ? '' : 's'} to ${path}`);
|
|
56
47
|
if (skipped > 0) {
|
|
57
|
-
this.warn(`${skipped
|
|
48
|
+
this.warn(`${pluralize(skipped, 'form')} skipped because they have no id`);
|
|
58
49
|
}
|
|
59
50
|
}
|
|
60
51
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
import { readdir, readFile, rename } from 'node:fs/promises';
|
|
2
|
+
import { readFile, rename } 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
7
|
import { FORM_ENDPOINT, getFormId, getFormTitle } from '../../utils/form-format.js';
|
|
8
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
9
|
import { toSlug } from '../../utils/to-slug.js';
|
|
9
10
|
export default class Push extends PushCommand {
|
|
10
11
|
static args = {
|
|
@@ -23,13 +24,10 @@ export default class Push extends PushCommand {
|
|
|
23
24
|
this.log(`Pushing forms to ${url}`);
|
|
24
25
|
this.log(`Forms path: ${path}`);
|
|
25
26
|
const files = await this.loadFiles(path);
|
|
26
|
-
this.log(`Found ${files.length
|
|
27
|
-
await
|
|
28
|
-
task: async (_ctx, task) => this.pushForm(filePath, data, task),
|
|
29
|
-
title: `Push ${getFormTitle(data)}`,
|
|
30
|
-
})), { concurrent: false, exitOnError: false }).run();
|
|
27
|
+
this.log(`Found ${pluralize(files.length, 'form')} to push`);
|
|
28
|
+
await this.runPushTasks(files, ({ data }) => getFormTitle(data), async ({ data, filePath }, task) => this.pushForm(filePath, data, task));
|
|
31
29
|
if (this.failedCount > 0) {
|
|
32
|
-
this.error(`${this.failedCount
|
|
30
|
+
this.error(`${pluralize(this.failedCount, 'form')} failed to push.`);
|
|
33
31
|
}
|
|
34
32
|
if (this.dryRun)
|
|
35
33
|
return;
|
|
@@ -49,15 +47,7 @@ export default class Push extends PushCommand {
|
|
|
49
47
|
// form, not abort loading the rest of the directory, same principle as loadObjects() in
|
|
50
48
|
// commands/acf/push.ts.
|
|
51
49
|
async loadFiles(dir) {
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
files = await readdir(dir);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (error.code === 'ENOENT')
|
|
58
|
-
return [];
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
50
|
+
const files = await readdirTolerant(dir);
|
|
61
51
|
const forms = [];
|
|
62
52
|
for (const file of files) {
|
|
63
53
|
if (extname(file) !== '.json')
|
|
@@ -79,8 +69,6 @@ export default class Push extends PushCommand {
|
|
|
79
69
|
}
|
|
80
70
|
return forms;
|
|
81
71
|
}
|
|
82
|
-
// PUT-then-404-fallback-POST is the same dance as commands/snippet/push.ts, forms are
|
|
83
|
-
// id-based like snippets, not key-based like ACF.
|
|
84
72
|
async pushForm(filePath, data, task) {
|
|
85
73
|
const title = getFormTitle(data);
|
|
86
74
|
if (this.dryRun) {
|
|
@@ -90,28 +78,15 @@ export default class Push extends PushCommand {
|
|
|
90
78
|
}
|
|
91
79
|
try {
|
|
92
80
|
const id = getFormId(data);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await this.ensureCanonicalFilename(filePath, id, title);
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
// The id recorded locally doesn't exist on this site (e.g. a fresh install): create
|
|
106
|
-
// it instead of failing, and adopt whatever id the site assigns.
|
|
107
|
-
if (!isNotFoundError(error))
|
|
108
|
-
throw error;
|
|
109
|
-
const created = await this.wp.post(FORM_ENDPOINT, data);
|
|
110
|
-
const newId = getFormId(created);
|
|
111
|
-
if (newId !== null)
|
|
112
|
-
await this.ensureCanonicalFilename(filePath, newId, title);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
81
|
+
const { body, created } = await putOrCreate(this.wp, {
|
|
82
|
+
id,
|
|
83
|
+
payload: data,
|
|
84
|
+
postEndpoint: FORM_ENDPOINT,
|
|
85
|
+
putEndpoint: (formId) => `${FORM_ENDPOINT}/${formId}`,
|
|
86
|
+
});
|
|
87
|
+
const canonicalId = created ? getFormId(body) : id;
|
|
88
|
+
if (canonicalId !== null)
|
|
89
|
+
await this.ensureCanonicalFilename(filePath, canonicalId, title);
|
|
115
90
|
if (task)
|
|
116
91
|
task.output = `Pushed: ${title}`;
|
|
117
92
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -8,11 +8,15 @@ export default class Login extends Command {
|
|
|
8
8
|
async run() {
|
|
9
9
|
const { email, token } = await this.waitForCallback();
|
|
10
10
|
authManager.setAuth({ email, savedAt: new Date().toISOString(), token });
|
|
11
|
-
|
|
11
|
+
const emailSuffix = email ? ` as ${email}` : '';
|
|
12
|
+
this.log(`\nLogged in${emailSuffix}. You're all set!`);
|
|
12
13
|
}
|
|
13
14
|
async waitForCallback() {
|
|
14
15
|
return waitForLocalCallback({
|
|
15
|
-
buildUrl
|
|
16
|
+
buildUrl(callbackBaseUrl) {
|
|
17
|
+
const callbackUrl = encodeURIComponent(`${callbackBaseUrl}/callback`);
|
|
18
|
+
return `${CONSOLE_URL}/cli-auth?callbackUrl=${callbackUrl}`;
|
|
19
|
+
},
|
|
16
20
|
handleRequest(url, { resolveWithPage, respondBadRequest }) {
|
|
17
21
|
const token = url.searchParams.get('token');
|
|
18
22
|
if (!token) {
|
package/dist/commands/logout.js
CHANGED
|
@@ -10,6 +10,7 @@ export default class Logout extends Command {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
authManager.clearAuth();
|
|
13
|
-
|
|
13
|
+
const emailSuffix = auth.email ? ` (${auth.email})` : '';
|
|
14
|
+
this.log(`Logged out${emailSuffix}.`);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
7
6
|
import { getPageContent, getPageId, getPageTitle, PAGE_ENDPOINT, PAGE_LIST_QUERY, pageFileBase, pickPageMeta } from '../../utils/page-format.js';
|
|
7
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
8
|
export default class Pull extends LoopressCommand {
|
|
9
9
|
static args = {
|
|
10
10
|
path: Args.string({ description: 'Path to pages directory (overrides project config)' }),
|
|
@@ -34,34 +34,23 @@ export default class Pull extends LoopressCommand {
|
|
|
34
34
|
key: numericPrefixKey,
|
|
35
35
|
});
|
|
36
36
|
const pulled = withId.map((page) => ({ id: getPageId(page), title: getPageTitle(page) }));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
await this.pullDirectory(path, withId, orphans, {
|
|
38
|
+
dryRunMessage: `Would pull ${pluralize(withId.length, 'page')} to ${path}`,
|
|
39
|
+
orphanReason: `in ${path} no longer present on WordPress`,
|
|
40
|
+
pulledMessage: `Pulled ${pluralize(withId.length, 'page')} to ${path}`,
|
|
41
|
+
title: (page) => getPageTitle(page),
|
|
42
|
+
async write(page, writeDir) {
|
|
43
|
+
const base = pageFileBase(getPageId(page), getPageTitle(page));
|
|
44
|
+
// `content` lives entirely in the `.html` file; the `.json` sidecar keeps only the
|
|
45
|
+
// fields WordPress actually accepts back on write, see pickPageMeta.
|
|
46
|
+
await writeFile(join(writeDir, `${base}.html`), getPageContent(page));
|
|
47
|
+
await writeFile(join(writeDir, `${base}.json`), JSON.stringify(pickPageMeta(page), null, 2) + '\n');
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (this.dryRun)
|
|
42
51
|
return { orphans, pulled, skipped, status: 'dry-run' };
|
|
43
|
-
}
|
|
44
|
-
if (withId.length > 0)
|
|
45
|
-
await mkdir(path, { recursive: true });
|
|
46
|
-
await new Listr(withId.map((page) => {
|
|
47
|
-
const id = getPageId(page);
|
|
48
|
-
const title = getPageTitle(page);
|
|
49
|
-
return {
|
|
50
|
-
async task(_ctx, task) {
|
|
51
|
-
const base = pageFileBase(id, title);
|
|
52
|
-
// `content` lives entirely in the `.html` file; the `.json` sidecar keeps only the
|
|
53
|
-
// fields WordPress actually accepts back on write, see pickPageMeta.
|
|
54
|
-
await writeFile(join(path, `${base}.html`), getPageContent(page));
|
|
55
|
-
await writeFile(join(path, `${base}.json`), JSON.stringify(pickPageMeta(page), null, 2) + '\n');
|
|
56
|
-
task.output = `Pulled: ${title}`;
|
|
57
|
-
},
|
|
58
|
-
title: `Pull ${title}`,
|
|
59
|
-
};
|
|
60
|
-
}), { renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
61
|
-
await this.removeOrphanedFiles(path, orphans, `in ${path} no longer present on WordPress`);
|
|
62
|
-
this.log(`Pulled ${withId.length} page${withId.length === 1 ? '' : 's'} to ${path}`);
|
|
63
52
|
if (skipped > 0) {
|
|
64
|
-
this.warn(`${skipped
|
|
53
|
+
this.warn(`${pluralize(skipped, 'page')} skipped because they have no id`);
|
|
65
54
|
}
|
|
66
55
|
return { orphans, pulled, skipped, status: 'success' };
|
|
67
56
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import {
|
|
3
|
-
import { readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { basename, 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
7
|
import { getPageId, getPageTitle, PAGE_ENDPOINT, pageFileBase } from '../../utils/page-format.js';
|
|
8
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
8
9
|
export default class Push extends PushCommand {
|
|
9
10
|
static args = {
|
|
10
11
|
path: Args.string({ description: 'Path to pages directory (overrides project config)' }),
|
|
@@ -23,17 +24,14 @@ export default class Push extends PushCommand {
|
|
|
23
24
|
this.log(`Pushing pages to ${url}`);
|
|
24
25
|
this.log(`Pages path: ${path}`);
|
|
25
26
|
const pages = await this.loadFiles(path);
|
|
26
|
-
this.log(`Found ${pages.length
|
|
27
|
+
this.log(`Found ${pluralize(pages.length, 'page')} to push`);
|
|
27
28
|
const pushed = [];
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
title: `Push ${getPageTitle(page.meta)}`,
|
|
34
|
-
})), { concurrent: false, exitOnError: false, renderer: this.jsonEnabled() ? 'silent' : 'default' }).run();
|
|
29
|
+
await this.runPushTasks(pages, (page) => getPageTitle(page.meta), async (page, task) => {
|
|
30
|
+
const id = await this.pushPage(page, task);
|
|
31
|
+
pushed.push({ id, title: getPageTitle(page.meta) });
|
|
32
|
+
});
|
|
35
33
|
if (this.failedCount > 0) {
|
|
36
|
-
this.error(`${this.failedCount
|
|
34
|
+
this.error(`${pluralize(this.failedCount, 'page')} failed to push.`);
|
|
37
35
|
}
|
|
38
36
|
if (this.dryRun)
|
|
39
37
|
return { pushed, status: 'dry-run' };
|
|
@@ -61,15 +59,7 @@ export default class Push extends PushCommand {
|
|
|
61
59
|
// corrupted or hand-broken sidecar must only skip that page, not abort loading the rest of
|
|
62
60
|
// the directory, same principle as commands/form/push.ts.
|
|
63
61
|
async loadFiles(dir) {
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
files = await readdir(dir);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
if (error.code === 'ENOENT')
|
|
70
|
-
return [];
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
62
|
+
const files = await readdirTolerant(dir);
|
|
73
63
|
const pages = [];
|
|
74
64
|
for (const file of files) {
|
|
75
65
|
if (extname(file) !== '.html')
|
|
@@ -103,8 +93,6 @@ export default class Push extends PushCommand {
|
|
|
103
93
|
}
|
|
104
94
|
return pages;
|
|
105
95
|
}
|
|
106
|
-
// PUT-then-404-fallback-POST, same dance as commands/form/push.ts: pages are id-based.
|
|
107
|
-
//
|
|
108
96
|
// ponytail: `parent` round-trips as the source site's numeric page id untouched, so pushing
|
|
109
97
|
// to a site where that id belongs to a different page (or nothing) silently mis-parents or
|
|
110
98
|
// orphans the page. No id-remapping across sites here; fine for same-hierarchy environments,
|
|
@@ -117,38 +105,25 @@ export default class Push extends PushCommand {
|
|
|
117
105
|
return getPageId(page.meta);
|
|
118
106
|
}
|
|
119
107
|
try {
|
|
120
|
-
|
|
108
|
+
const id = getPageId(page.meta);
|
|
121
109
|
const payload = { ...page.meta, content: page.content };
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// rejects any POST that still carries a (now stale) `id` field with a 400 "Cannot
|
|
137
|
-
// create existing post", regardless of whether that id actually exists, so it must
|
|
138
|
-
// be stripped before falling back to create.
|
|
139
|
-
if (!isNotFoundError(error))
|
|
140
|
-
throw error;
|
|
141
|
-
const newPayload = { ...payload };
|
|
142
|
-
delete newPayload.id;
|
|
143
|
-
const created = await this.wp.post(PAGE_ENDPOINT, newPayload);
|
|
144
|
-
id = getPageId(created);
|
|
145
|
-
if (id !== null)
|
|
146
|
-
await this.ensureCanonicalFilename(page, id, title);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
110
|
+
// WordPress core rejects any POST that still carries a (now stale) `id` field with a
|
|
111
|
+
// 400 "Cannot create existing post", regardless of whether that id actually exists.
|
|
112
|
+
const postPayload = { ...payload };
|
|
113
|
+
delete postPayload.id;
|
|
114
|
+
const { body, created } = await putOrCreate(this.wp, {
|
|
115
|
+
id,
|
|
116
|
+
payload,
|
|
117
|
+
postEndpoint: PAGE_ENDPOINT,
|
|
118
|
+
postPayload,
|
|
119
|
+
putEndpoint: (pageId) => `${PAGE_ENDPOINT}/${pageId}`,
|
|
120
|
+
});
|
|
121
|
+
const resultId = created ? getPageId(body) : id;
|
|
122
|
+
if (resultId !== null)
|
|
123
|
+
await this.ensureCanonicalFilename(page, resultId, title);
|
|
149
124
|
if (task)
|
|
150
125
|
task.output = `Pushed: ${title}`;
|
|
151
|
-
return
|
|
126
|
+
return resultId;
|
|
152
127
|
}
|
|
153
128
|
catch (error) {
|
|
154
129
|
this.reportTaskFailure(`Failed to push ${title}: ${error.message}`, error, task);
|