@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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { watch as watchFiles } from 'chokidar';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { configManager } from '../config/project-config.manager.js';
|
|
5
|
+
import { buildWatchTargets, createDebouncedBatcher, resolveResourceTypes, RESOURCE_TYPES, resourceTypeForPath } from '../lib/dev-watch.js';
|
|
6
|
+
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
7
|
+
const DEBOUNCE_MS = 400;
|
|
8
|
+
// Noise no one wants pushed: VCS internals, deps, and editor/OS droppings.
|
|
9
|
+
const IGNORED = /(?:(^|[/\\])(\.git|node_modules|\.DS_Store)(?:[/\\]|$))|(?:\.swp$)/;
|
|
10
|
+
export default class Dev extends Command {
|
|
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
|
+
static examples = ['$ lps dev', '$ lps dev --only=snippets,pages', '$ lps dev --skip=plugins'];
|
|
13
|
+
static flags = {
|
|
14
|
+
only: Flags.string({ description: `Only watch these resource types (comma-separated): ${RESOURCE_TYPES.join(', ')}` }),
|
|
15
|
+
skip: Flags.string({ description: `Skip these resource types (comma-separated): ${RESOURCE_TYPES.join(', ')}` }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(Dev);
|
|
19
|
+
const only = flags.only ? this.parseTypeFlag(flags.only, 'only') : undefined;
|
|
20
|
+
const skip = flags.skip ? this.parseTypeFlag(flags.skip, 'skip') : [];
|
|
21
|
+
const types = resolveResourceTypes(only, skip);
|
|
22
|
+
const localConfig = await readLocalConfig();
|
|
23
|
+
const projectId = localConfig.projectId ?? configManager.getCurrentProject()?.id;
|
|
24
|
+
if (!projectId)
|
|
25
|
+
this.error('No project configured. Run `lps project config` first.');
|
|
26
|
+
if (!configManager.getEnvironment(projectId, 'local')) {
|
|
27
|
+
this.error('No "local" environment configured for this project. Run `lps project config` and add a "local" environment.');
|
|
28
|
+
}
|
|
29
|
+
const targets = buildWatchTargets(types, localConfig, process.cwd());
|
|
30
|
+
if (targets.length === 0) {
|
|
31
|
+
this.error('Nothing to watch: none of the selected resource directories exist yet.');
|
|
32
|
+
}
|
|
33
|
+
for (const target of targets)
|
|
34
|
+
this.log(`Watching ${target.type}: ${target.path}`);
|
|
35
|
+
await this.watch(targets);
|
|
36
|
+
}
|
|
37
|
+
parseTypeFlag(raw, flagName) {
|
|
38
|
+
return raw
|
|
39
|
+
.split(',')
|
|
40
|
+
.map((value) => value.trim())
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((value) => {
|
|
43
|
+
if (!RESOURCE_TYPES.includes(value)) {
|
|
44
|
+
this.error(`Unknown resource type "${value}" in --${flagName}. Valid types: ${RESOURCE_TYPES.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Sequential on purpose (see notes technique: parallelizing push is out of scope). A failed
|
|
50
|
+
// push is logged and swallowed here, never thrown, so one bad batch can't take the watcher down.
|
|
51
|
+
async pushBatch(changesByType, targets) {
|
|
52
|
+
for (const [type, paths] of changesByType) {
|
|
53
|
+
const target = targets.find((t) => t.type === type);
|
|
54
|
+
if (!target)
|
|
55
|
+
continue;
|
|
56
|
+
this.log(`\n→ ${type} changed (${paths.map((path) => basename(path)).join(', ')}), pushing to local...`);
|
|
57
|
+
try {
|
|
58
|
+
await this.config.runCommand(target.commandId, ['--env', 'local']);
|
|
59
|
+
this.log(`✓ ${type} synced`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
this.log(`✗ ${type} failed: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async watch(targets) {
|
|
67
|
+
const batcher = createDebouncedBatcher(async (paths) => {
|
|
68
|
+
const changesByType = new Map();
|
|
69
|
+
for (const path of paths) {
|
|
70
|
+
const type = resourceTypeForPath(path, targets);
|
|
71
|
+
if (!type)
|
|
72
|
+
continue;
|
|
73
|
+
changesByType.set(type, [...(changesByType.get(type) ?? []), path]);
|
|
74
|
+
}
|
|
75
|
+
await this.pushBatch(changesByType, targets);
|
|
76
|
+
}, DEBOUNCE_MS);
|
|
77
|
+
const watcher = watchFiles(targets.map((target) => target.path), {
|
|
78
|
+
awaitWriteFinish: { pollInterval: 100, stabilityThreshold: 300 },
|
|
79
|
+
ignoreInitial: true,
|
|
80
|
+
ignored: (path) => IGNORED.test(path),
|
|
81
|
+
});
|
|
82
|
+
watcher
|
|
83
|
+
.on('add', batcher.queue)
|
|
84
|
+
.on('change', batcher.queue)
|
|
85
|
+
.on('unlink', (filePath) => { this.log(`⚠ ${filePath} deleted locally, not synced automatically`); });
|
|
86
|
+
this.log('\nWatching for changes. Press Ctrl+C to stop.\n');
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
process.once('SIGINT', () => { resolve(); });
|
|
89
|
+
});
|
|
90
|
+
batcher.cancel();
|
|
91
|
+
await watcher.close();
|
|
92
|
+
// A push failure mid-session sets process.exitCode; stopping the watcher on purpose is a
|
|
93
|
+
// clean exit regardless of that history (see AC: no meaningful exit code needed for `dev`).
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
this.log('\nStopped watching.');
|
|
96
|
+
}
|
|
97
|
+
}
|
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'];
|
|
@@ -15,12 +16,12 @@ export default class Doctor extends LoopressCommand {
|
|
|
15
16
|
this.log(`URL: ${url}`);
|
|
16
17
|
this.log('');
|
|
17
18
|
let failed = 0;
|
|
18
|
-
const
|
|
19
|
+
const isReachable = await this.check('WordPress REST API reachable', async () => {
|
|
19
20
|
const diagnostic = await diagnoseWpSite(url);
|
|
20
21
|
if (!diagnostic.ok)
|
|
21
22
|
throw new Error(diagnostic.reason);
|
|
22
23
|
});
|
|
23
|
-
if (!
|
|
24
|
+
if (!isReachable) {
|
|
24
25
|
failed++;
|
|
25
26
|
this.log('- Remaining checks skipped while the site is unreachable.');
|
|
26
27
|
this.error('1 check failed.', { exit: 1 });
|
|
@@ -34,18 +35,18 @@ export default class Doctor extends LoopressCommand {
|
|
|
34
35
|
// The namespace index answers regardless of which Loopress features are active, so a 404
|
|
35
36
|
// here can only mean the plugin itself is missing or outdated (WpClient's 404 message
|
|
36
37
|
// already says exactly that).
|
|
37
|
-
if (!(await this.check('Loopress plugin installed (loopress/v1 endpoints)', () => this.wp.get('loopress/v1')))) {
|
|
38
|
+
if (!(await this.check('Loopress plugin installed (loopress/v1 endpoints)', async () => this.wp.get('loopress/v1')))) {
|
|
38
39
|
failed++;
|
|
39
40
|
}
|
|
40
41
|
// wp/v2/users/me is WordPress core and requires authentication, so it validates the
|
|
41
42
|
// application password without depending on any Loopress feature.
|
|
42
|
-
if (!(await this.check('Credentials accepted (authenticated request)', () => this.wp.get('wp/v2/users/me')))) {
|
|
43
|
+
if (!(await this.check('Credentials accepted (authenticated request)', async () => this.wp.get('wp/v2/users/me')))) {
|
|
43
44
|
failed++;
|
|
44
45
|
}
|
|
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/init.js
CHANGED
|
@@ -29,11 +29,11 @@ export default class Init extends Command {
|
|
|
29
29
|
}
|
|
30
30
|
const configPath = join(process.cwd(), 'loopress.json');
|
|
31
31
|
if (existsSync(configPath)) {
|
|
32
|
-
const
|
|
32
|
+
const isOverwrite = await confirm({
|
|
33
33
|
default: false,
|
|
34
34
|
message: 'loopress.json already exists. Overwrite?',
|
|
35
35
|
});
|
|
36
|
-
if (!
|
|
36
|
+
if (!isOverwrite) {
|
|
37
37
|
this.log('Aborted.');
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
@@ -65,11 +65,11 @@ export default class Init extends Command {
|
|
|
65
65
|
config[feature.key] = feature.dir;
|
|
66
66
|
}
|
|
67
67
|
await writeLocalConfig(config);
|
|
68
|
-
let
|
|
68
|
+
let isProviderAdded = false;
|
|
69
69
|
if (providerChoice !== '__none__') {
|
|
70
70
|
try {
|
|
71
71
|
await this.config.runCommand('plugin:add', [providerChoice]);
|
|
72
|
-
|
|
72
|
+
isProviderAdded = true;
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
75
75
|
this.warn(error.message);
|
|
@@ -83,7 +83,7 @@ export default class Init extends Command {
|
|
|
83
83
|
this.log(` ${feature.label}:${' '.repeat(Math.max(1, 9 - feature.label.length))}${join(rootDir, feature.dir)}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
if (
|
|
86
|
+
if (isProviderAdded) {
|
|
87
87
|
this.log(` Plugin: ${providerChoice}`);
|
|
88
88
|
}
|
|
89
89
|
this.log('\n→ Next: run `lps snippet pull` to fetch what is already on the site, or `lps doctor` to verify the connection.');
|
|
@@ -102,8 +102,8 @@ export default class Init extends Command {
|
|
|
102
102
|
let projects = configManager.listProjects();
|
|
103
103
|
if (projects.length === 0) {
|
|
104
104
|
this.log('No projects configured yet.');
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
105
|
+
const isRunConfig = await confirm({ default: true, message: 'Run `lps project config` now to add one?' });
|
|
106
|
+
if (!isRunConfig) {
|
|
107
107
|
return this.promptManualProjectId();
|
|
108
108
|
}
|
|
109
109
|
await this.config.runCommand('project:config');
|
package/dist/commands/login.js
CHANGED
|
@@ -8,21 +8,25 @@ 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
|
-
waitForCallback() {
|
|
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
|
-
const email = url.searchParams.get('email') ?? undefined;
|
|
19
22
|
if (!token) {
|
|
20
23
|
respondBadRequest('Missing token');
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
26
|
+
const email = url.searchParams.get('email') ?? undefined;
|
|
23
27
|
resolveWithPage(SUCCESS_PAGE, { email, token });
|
|
24
28
|
},
|
|
25
|
-
log: (message) => this.log(message),
|
|
29
|
+
log: (message) => { this.log(message); },
|
|
26
30
|
openingMessage: 'Opening Loopress console in your browser...',
|
|
27
31
|
timeoutMessage: 'Login timed out after 5 minutes. Please try again.',
|
|
28
32
|
});
|
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,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 { getPageId, getPageTitle, PAGE_ENDPOINT, PAGE_LIST_QUERY } from '../../utils/page-format.js';
|
|
4
3
|
export default class List extends LoopressCommand {
|
|
5
4
|
static description = 'List pages from WordPress';
|
|
5
|
+
static enableJsonFlag = true;
|
|
6
6
|
static examples = ['$ lps page 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 pages = await this.wp.get(`${PAGE_ENDPOINT}?${PAGE_LIST_QUERY}`);
|
|
13
|
-
if (flags.json) {
|
|
14
|
-
this.log(JSON.stringify(pages, null, 2));
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
9
|
this.log(`Pages (${pages.length}):`);
|
|
18
10
|
if (pages.length === 0) {
|
|
19
11
|
this.log(' (none)');
|
|
20
|
-
return;
|
|
12
|
+
return pages;
|
|
21
13
|
}
|
|
22
14
|
for (const page of pages) {
|
|
23
15
|
this.log(` ${getPageId(page) ?? '(no id)'}. ${getPageTitle(page)}`);
|
|
24
16
|
}
|
|
17
|
+
return pages;
|
|
25
18
|
}
|
|
26
19
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type PulledPage = {
|
|
3
|
+
id: number;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
type PullResult = {
|
|
7
|
+
orphans: string[];
|
|
8
|
+
pulled: PulledPage[];
|
|
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,15 +1,16 @@
|
|
|
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)' }),
|
|
11
11
|
};
|
|
12
12
|
static description = 'Pull pages from WordPress';
|
|
13
|
+
static enableJsonFlag = true;
|
|
13
14
|
static examples = ['$ lps page pull'];
|
|
14
15
|
static flags = {
|
|
15
16
|
...LoopressCommand.dryRunFlag,
|
|
@@ -32,34 +33,25 @@ export default class Pull extends LoopressCommand {
|
|
|
32
33
|
extensions: ['.json', '.html'],
|
|
33
34
|
key: numericPrefixKey,
|
|
34
35
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// fields WordPress actually accepts back on write, see pickPageMeta.
|
|
52
|
-
await writeFile(join(path, `${base}.html`), getPageContent(page));
|
|
53
|
-
await writeFile(join(path, `${base}.json`), JSON.stringify(pickPageMeta(page), null, 2) + '\n');
|
|
54
|
-
task.output = `Pulled: ${title}`;
|
|
55
|
-
},
|
|
56
|
-
title: `Pull ${title}`,
|
|
57
|
-
};
|
|
58
|
-
})).run();
|
|
59
|
-
await this.removeOrphanedFiles(path, orphans, `in ${path} no longer present on WordPress`);
|
|
60
|
-
this.log(`Pulled ${withId.length} page${withId.length === 1 ? '' : 's'} to ${path}`);
|
|
36
|
+
const pulled = withId.map((page) => ({ id: getPageId(page), title: getPageTitle(page) }));
|
|
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)
|
|
51
|
+
return { orphans, pulled, skipped, status: 'dry-run' };
|
|
61
52
|
if (skipped > 0) {
|
|
62
|
-
this.warn(`${skipped
|
|
53
|
+
this.warn(`${pluralize(skipped, 'page')} skipped because they have no id`);
|
|
63
54
|
}
|
|
55
|
+
return { orphans, pulled, skipped, status: 'success' };
|
|
64
56
|
}
|
|
65
57
|
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type PushedPage = {
|
|
3
|
+
id: null | number;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
type PushResult = {
|
|
7
|
+
pushed: PushedPage[];
|
|
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 loadFiles;
|
|
15
24
|
private pushPage;
|
|
16
25
|
}
|
|
26
|
+
export {};
|