@loopress/cli 0.7.0 → 0.8.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.
Files changed (39) hide show
  1. package/README.md +45 -45
  2. package/dist/commands/composer/pull.js +4 -14
  3. package/dist/commands/composer/push.js +7 -16
  4. package/dist/commands/login.js +1 -1
  5. package/dist/commands/logout.js +1 -1
  6. package/dist/commands/plugin/add.js +9 -9
  7. package/dist/commands/plugin/pull.js +7 -18
  8. package/dist/commands/plugin/push.d.ts +1 -0
  9. package/dist/commands/plugin/push.js +20 -45
  10. package/dist/commands/snippet/list.d.ts +1 -1
  11. package/dist/commands/snippet/list.js +23 -38
  12. package/dist/commands/snippet/pull.d.ts +1 -1
  13. package/dist/commands/snippet/pull.js +41 -50
  14. package/dist/commands/snippet/push.d.ts +1 -1
  15. package/dist/commands/snippet/push.js +82 -70
  16. package/dist/config/auth.manager.d.ts +0 -2
  17. package/dist/config/auth.manager.js +5 -25
  18. package/dist/config/json-file.d.ts +2 -0
  19. package/dist/config/json-file.js +21 -0
  20. package/dist/config/project-config.manager.d.ts +1 -3
  21. package/dist/config/project-config.manager.js +7 -23
  22. package/dist/lib/base.d.ts +14 -4
  23. package/dist/lib/base.js +63 -33
  24. package/dist/lib/push-command.d.ts +0 -1
  25. package/dist/lib/push-command.js +0 -1
  26. package/dist/lib/wp-client.d.ts +15 -0
  27. package/dist/lib/wp-client.js +53 -0
  28. package/dist/types/snippet.d.ts +7 -1
  29. package/dist/utils/loopress-config.js +5 -2
  30. package/dist/utils/snippet-plugin-flag.d.ts +3 -0
  31. package/dist/utils/snippet-plugin-flag.js +8 -0
  32. package/dist/utils/snippet-plugin.d.ts +23 -2
  33. package/dist/utils/snippet-plugin.js +168 -13
  34. package/oclif.manifest.json +165 -125
  35. package/package.json +5 -3
  36. package/dist/types/menu.d.ts +0 -7
  37. package/dist/types/menu.js +0 -1
  38. /package/dist/{config/types.d.ts → types/config.d.ts} +0 -0
  39. /package/dist/{config/types.js → types/config.js} +0 -0
package/dist/lib/base.js CHANGED
@@ -2,70 +2,100 @@ import { Command, Flags } from '@oclif/core';
2
2
  import { join } from 'node:path';
3
3
  import { configManager } from '../config/project-config.manager.js';
4
4
  import { readLocalConfig } from '../utils/loopress-config.js';
5
+ import { WpClient } from './wp-client.js';
5
6
  export class LoopressCommand extends Command {
6
7
  static baseFlags = {
7
8
  password: Flags.string({
8
- description: 'WordPress application password (fallback; prefer `lps project config`)',
9
+ description: 'WordPress application password (overrides project config, requires --user)',
9
10
  helpGroup: 'GLOBAL',
10
11
  }),
11
12
  url: Flags.string({
12
- description: 'WordPress URL (fallback; prefer `lps project config`)',
13
+ description: 'WordPress URL (overrides project config)',
13
14
  helpGroup: 'GLOBAL',
14
15
  }),
15
16
  user: Flags.string({
16
- description: 'WordPress username (fallback; prefer `lps project config`)',
17
+ description: 'WordPress username (overrides project config, requires --password)',
17
18
  helpGroup: 'GLOBAL',
18
19
  }),
19
20
  };
21
+ static dryRunFlag = {
22
+ 'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
23
+ };
24
+ dryRun = false;
25
+ localConfig = {};
20
26
  siteConfig;
21
- async buildAuthHeaders() {
22
- const { token, url } = this.siteConfig;
23
- if (token) {
24
- return { Authorization: `Basic ${Buffer.from(token).toString('base64')}` };
27
+ wpClient;
28
+ get rootDir() {
29
+ return this.localConfig.rootDir ?? '.';
30
+ }
31
+ get wp() {
32
+ if (!this.wpClient) {
33
+ const { token, url } = this.siteConfig;
34
+ if (!token) {
35
+ this.error(`No credentials configured for ${url}. Run \`lps project config\` to add them.`);
36
+ }
37
+ this.wpClient = new WpClient(url, token);
25
38
  }
26
- this.error(`No credentials configured for ${url}. Run \`lps project config\` to add them.`);
39
+ return this.wpClient;
27
40
  }
28
41
  async init() {
29
42
  await super.init();
30
- const localConfig = await readLocalConfig();
31
- if (localConfig.projectId) {
32
- const project = configManager.getProject(localConfig.projectId);
43
+ const { flags } = (await this.parse({
44
+ args: this.ctor.args,
45
+ flags: this.ctor.flags,
46
+ strict: this.ctor.strict,
47
+ }));
48
+ this.dryRun = Boolean(flags['dry-run']);
49
+ this.localConfig = await readLocalConfig();
50
+ if (Boolean(flags.user) !== Boolean(flags.password)) {
51
+ this.error('--user and --password must be provided together.');
52
+ }
53
+ const flagToken = flags.user && flags.password ? `${flags.user}:${flags.password}` : undefined;
54
+ if (flags.url) {
55
+ this.siteConfig = {
56
+ addedAt: new Date().toISOString(),
57
+ name: 'cli-flags',
58
+ token: flagToken,
59
+ url: flags.url.replace(/\/+$/, ''),
60
+ };
61
+ return;
62
+ }
63
+ const env = this.resolveEnvironment();
64
+ this.siteConfig = flagToken ? { ...env, token: flagToken } : env;
65
+ }
66
+ resolveSnippetPlugin(flag) {
67
+ if (flag)
68
+ return flag;
69
+ return this.localConfig.snippetPlugin ?? 'wpcode';
70
+ }
71
+ resolveSnippetsPath(override) {
72
+ if (override)
73
+ return override;
74
+ return join(this.rootDir, this.localConfig.snippetsDir ?? 'snippets');
75
+ }
76
+ resolveEnvironment() {
77
+ if (this.localConfig.projectId) {
78
+ const project = configManager.getProject(this.localConfig.projectId);
33
79
  if (!project) {
34
- this.error(`Project "${localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
80
+ this.error(`Project "${this.localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
35
81
  }
36
82
  const envNames = Object.keys(project.environments);
37
83
  if (envNames.length === 0) {
38
84
  this.error(`Project "${project.name}" has no environments configured. Run \`lps project config\` to add one.`);
39
85
  }
40
86
  if (envNames.length === 1) {
41
- this.siteConfig = project.environments[envNames[0]];
42
- return;
87
+ return project.environments[envNames[0]];
43
88
  }
44
89
  const current = configManager.getCurrentProject();
45
- const currentEnv = current?.id === localConfig.projectId ? configManager.getCurrentEnv() : null;
90
+ const currentEnv = current?.id === this.localConfig.projectId ? configManager.getCurrentEnv() : null;
46
91
  if (!currentEnv) {
47
92
  this.error(`Project "${project.name}" has multiple environments. Run \`lps project switch\` to pick one.`);
48
93
  }
49
- this.siteConfig = currentEnv;
50
- return;
94
+ return currentEnv;
51
95
  }
52
96
  const env = configManager.getCurrentEnv();
53
- if (env) {
54
- this.siteConfig = env;
55
- return;
56
- }
97
+ if (env)
98
+ return env;
57
99
  this.error('No environment configured. Run `lps project config` first.');
58
100
  }
59
- async resolveSnippetPlugin(flag) {
60
- if (flag)
61
- return flag;
62
- const config = await readLocalConfig();
63
- return config.snippetPlugin ?? 'wpcode';
64
- }
65
- async resolveSnippetsPath(override) {
66
- if (override)
67
- return override;
68
- const config = await readLocalConfig();
69
- return join(config.rootDir ?? '.', config.snippetsDir ?? 'snippets');
70
- }
71
101
  }
@@ -1,6 +1,5 @@
1
1
  import { LoopressCommand } from './base.js';
2
2
  export declare abstract class PushCommand extends LoopressCommand {
3
- protected dryRun: boolean;
4
3
  catch(err: Error): Promise<void>;
5
4
  protected recordDeployment(status: 'failure' | 'success'): Promise<void>;
6
5
  protected recordSuccess(): Promise<void>;
@@ -3,7 +3,6 @@ import { authManager } from '../config/auth.manager.js';
3
3
  import { LoopressCommand } from './base.js';
4
4
  const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
5
5
  export class PushCommand extends LoopressCommand {
6
- dryRun = false;
7
6
  async catch(err) {
8
7
  if (!this.dryRun && this.siteConfig) {
9
8
  await this.recordDeployment('failure');
@@ -0,0 +1,15 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ /**
3
+ * HTTP client for a WordPress site's REST API.
4
+ * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
5
+ */
6
+ export declare class WpClient {
7
+ private readonly siteUrl;
8
+ private readonly client;
9
+ constructor(siteUrl: string, token: string);
10
+ get<T>(path: string): Promise<T>;
11
+ post<T = unknown>(path: string, json?: Record<string, unknown>): Promise<T>;
12
+ put<T = unknown>(path: string, json?: Record<string, unknown>): Promise<T>;
13
+ private request;
14
+ }
15
+ export declare function formatWpError(error: unknown, url: string): string;
@@ -0,0 +1,53 @@
1
+ import got from 'got';
2
+ export const REQUEST_TIMEOUT_MS = 30_000;
3
+ /**
4
+ * HTTP client for a WordPress site's REST API.
5
+ * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
6
+ */
7
+ export class WpClient {
8
+ siteUrl;
9
+ client;
10
+ constructor(siteUrl, token) {
11
+ this.siteUrl = siteUrl;
12
+ this.client = got.extend({
13
+ headers: { Authorization: `Basic ${Buffer.from(token).toString('base64')}` },
14
+ prefixUrl: `${siteUrl}/wp-json`,
15
+ timeout: { request: REQUEST_TIMEOUT_MS },
16
+ });
17
+ }
18
+ async get(path) {
19
+ return this.request('get', path);
20
+ }
21
+ async post(path, json) {
22
+ return this.request('post', path, json);
23
+ }
24
+ async put(path, json) {
25
+ return this.request('put', path, json);
26
+ }
27
+ async request(method, path, json) {
28
+ try {
29
+ const response = await this.client(path, { json, method });
30
+ return (response.body ? JSON.parse(response.body) : undefined);
31
+ }
32
+ catch (error) {
33
+ throw new Error(formatWpError(error, `${this.siteUrl}/wp-json/${path}`), { cause: error });
34
+ }
35
+ }
36
+ }
37
+ export function formatWpError(error, url) {
38
+ const err = error;
39
+ const status = err.response?.statusCode;
40
+ if (status === 401 || status === 403) {
41
+ return `Authentication failed (${status}) on ${url}. Check your credentials with \`lps project config\`.`;
42
+ }
43
+ if (status === 404) {
44
+ return `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
45
+ }
46
+ if (status !== undefined) {
47
+ return `Request failed (${status}) on ${url}.`;
48
+ }
49
+ if (err.name === 'TimeoutError') {
50
+ return `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s on ${url}. Is the site reachable?`;
51
+ }
52
+ return `Request to ${url} failed: ${err.message ?? String(error)}`;
53
+ }
@@ -1,8 +1,14 @@
1
- import { SnippetType } from '../utils/snippet-plugin.js';
1
+ import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-plugin.js';
2
2
  export interface Snippet {
3
+ active: boolean;
3
4
  code: string;
4
5
  id?: number;
6
+ insertMethod: SnippetInsertMethod;
7
+ location: SnippetLocation;
5
8
  name: string;
6
9
  path: string;
10
+ priority: number;
11
+ shortcodeAttributes: string[];
12
+ tags: string[];
7
13
  type: SnippetType;
8
14
  }
@@ -1,16 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ // Only a missing file is treated as "no config" (returns {}). A file that exists but
5
+ // fails to read or parse throws, so callers don't silently fall back to the global
6
+ // current environment when loopress.json is actually broken.
4
7
  export async function readLocalConfig() {
5
8
  const configPath = join(process.cwd(), 'loopress.json');
6
9
  if (!existsSync(configPath))
7
10
  return {};
11
+ const content = await readFile(configPath, 'utf8');
8
12
  try {
9
- const content = await readFile(configPath, 'utf8');
10
13
  return JSON.parse(content);
11
14
  }
12
15
  catch {
13
- return {};
16
+ throw new Error('loopress.json is not valid JSON. Fix or delete it, then run `lps init` again.');
14
17
  }
15
18
  }
16
19
  export async function writeLocalConfig(config) {
@@ -0,0 +1,3 @@
1
+ export declare const snippetPluginFlag: {
2
+ plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
3
+ };
@@ -0,0 +1,8 @@
1
+ import { Flags } from '@oclif/core';
2
+ export const snippetPluginFlag = {
3
+ plugin: Flags.string({
4
+ char: 'p',
5
+ description: 'WordPress snippet plugin to target (overrides loopress.json)',
6
+ options: ['code-snippets', 'wpcode'],
7
+ }),
8
+ };
@@ -1,18 +1,39 @@
1
1
  export type PluginName = 'code-snippets' | 'wpcode';
2
2
  export type SnippetType = 'css' | 'html' | 'js' | 'php' | 'text';
3
+ export type SnippetInsertMethod = 'auto' | 'shortcode';
4
+ export type SnippetLocation = 'admin' | 'body' | 'everywhere' | 'footer' | 'frontend' | 'header' | 'once';
3
5
  export interface NormalizedSnippet {
4
6
  active: boolean;
5
7
  code: string;
6
8
  description: string;
7
9
  id: number;
10
+ insertMethod: SnippetInsertMethod;
11
+ location: SnippetLocation;
8
12
  name: string;
13
+ priority: number;
14
+ shortcodeAttributes: string[];
9
15
  tags: string[];
10
16
  type: SnippetType;
11
17
  }
12
18
  export declare function parseType(raw: unknown): null | SnippetType;
19
+ export declare function parseLocation(raw: unknown): null | SnippetLocation;
20
+ export declare function parseInsertMethod(raw: unknown): null | SnippetInsertMethod;
21
+ export declare function defaultLocationForType(type: SnippetType): SnippetLocation;
22
+ export interface SnippetPayloadInput {
23
+ active: boolean;
24
+ code: string;
25
+ insertMethod: SnippetInsertMethod;
26
+ location: SnippetLocation;
27
+ name: string;
28
+ path: string;
29
+ priority: number;
30
+ shortcodeAttributes: string[];
31
+ tags: string[];
32
+ type: SnippetType;
33
+ }
13
34
  export interface SnippetPlugin {
14
- endpoint(siteUrl: string): string;
35
+ endpointPath(): string;
15
36
  fromRemote(data: Record<string, unknown>): NormalizedSnippet;
16
- toPayload(name: string, code: string, path: string, type: SnippetType): Record<string, unknown>;
37
+ toPayload(snippet: SnippetPayloadInput): Record<string, unknown>;
17
38
  }
18
39
  export declare function getSnippetPlugin(name: PluginName): SnippetPlugin;
@@ -3,6 +3,30 @@ export function parseType(raw) {
3
3
  const value = String(raw ?? '').toLowerCase();
4
4
  return valid.includes(value) ? value : null;
5
5
  }
6
+ const VALID_LOCATIONS = new Set(['admin', 'body', 'everywhere', 'footer', 'frontend', 'header', 'once']);
7
+ export function parseLocation(raw) {
8
+ const value = String(raw ?? '').toLowerCase();
9
+ return VALID_LOCATIONS.has(value) ? value : null;
10
+ }
11
+ export function parseInsertMethod(raw) {
12
+ return raw === 'auto' || raw === 'shortcode' ? raw : null;
13
+ }
14
+ // The sensible default placement for a freshly pushed snippet that doesn't specify a location.
15
+ export function defaultLocationForType(type) {
16
+ switch (type) {
17
+ case 'css': {
18
+ return 'header';
19
+ }
20
+ case 'html':
21
+ case 'js':
22
+ case 'text': {
23
+ return 'footer';
24
+ }
25
+ case 'php': {
26
+ return 'everywhere';
27
+ }
28
+ }
29
+ }
6
30
  function inferTypeFromCode(code) {
7
31
  const firstLine = code.trimStart().split('\n')[0].trimStart();
8
32
  if (firstLine.startsWith('<?'))
@@ -14,53 +38,184 @@ function inferTypeFromCode(code) {
14
38
  function resolveType(raw, code) {
15
39
  return parseType(raw) ?? inferTypeFromCode(code);
16
40
  }
41
+ function resolvePriority(raw) {
42
+ const value = Number(raw);
43
+ return Number.isFinite(value) ? value : 10;
44
+ }
45
+ // Snippet files on disk keep the <?php opening tag so they're valid, syntax-highlighted PHP files.
46
+ // Both plugins store just the executable body, so it's stripped before push and restored on pull
47
+ // (see buildSnippetFile in commands/snippet/pull.ts).
48
+ function stripPhpOpeningTag(code) {
49
+ return code.replace(/^<\?php\s*/i, '');
50
+ }
51
+ // The real Code Snippets plugin has no independent "type" field: its REST API only has `scope`,
52
+ // and the snippet type is derived from it (see WPCode_Snippet::get_type_from_scope() upstream).
53
+ // Sending a `type` key (as this adapter used to) is silently ignored by that plugin.
54
+ const CODE_SNIPPETS_SCOPE_TO_LOCATION = {
55
+ admin: 'admin',
56
+ 'admin-css': 'admin',
57
+ content: 'everywhere',
58
+ 'footer-content': 'footer',
59
+ 'front-end': 'frontend',
60
+ global: 'everywhere',
61
+ 'head-content': 'header',
62
+ 'single-use': 'once',
63
+ 'site-css': 'frontend',
64
+ 'site-footer-js': 'footer',
65
+ 'site-head-js': 'header',
66
+ };
67
+ function typeFromScope(scope) {
68
+ if (scope.endsWith('-css'))
69
+ return 'css';
70
+ if (scope.endsWith('-js'))
71
+ return 'js';
72
+ if (scope.endsWith('content'))
73
+ return 'html';
74
+ return 'php';
75
+ }
76
+ function scopeFromTypeAndLocation(type, location) {
77
+ switch (type) {
78
+ case 'css': {
79
+ if (location === 'frontend')
80
+ return 'site-css';
81
+ if (location === 'admin')
82
+ return 'admin-css';
83
+ throw new Error(`Code Snippets does not support the "${location}" location for CSS snippets. Use one of: frontend, admin.`);
84
+ }
85
+ case 'html': {
86
+ if (location === 'header')
87
+ return 'head-content';
88
+ if (location === 'footer')
89
+ return 'footer-content';
90
+ if (location === 'everywhere')
91
+ return 'content';
92
+ throw new Error(`Code Snippets does not support the "${location}" location for HTML snippets. Use one of: header, footer, everywhere.`);
93
+ }
94
+ case 'js': {
95
+ if (location === 'header')
96
+ return 'site-head-js';
97
+ if (location === 'footer')
98
+ return 'site-footer-js';
99
+ throw new Error(`Code Snippets does not support the "${location}" location for JS snippets. Use one of: header, footer.`);
100
+ }
101
+ case 'php': {
102
+ if (location === 'everywhere')
103
+ return 'global';
104
+ if (location === 'frontend')
105
+ return 'front-end';
106
+ if (location === 'admin')
107
+ return 'admin';
108
+ if (location === 'once')
109
+ return 'single-use';
110
+ throw new Error(`Code Snippets does not support the "${location}" location for PHP snippets. Use one of: everywhere, frontend, admin, once.`);
111
+ }
112
+ case 'text': {
113
+ throw new Error('Code Snippets has no "text" snippet type. Change the sidecar "type", or push this snippet to "wpcode" instead.');
114
+ }
115
+ }
116
+ }
17
117
  class CodeSnippetsPlugin {
18
- endpoint(siteUrl) {
19
- return `${siteUrl}/wp-json/code-snippets/v1/snippets`;
118
+ endpointPath() {
119
+ return 'code-snippets/v1/snippets';
20
120
  }
21
121
  fromRemote(data) {
122
+ const scope = String(data.scope ?? 'global');
123
+ const type = typeFromScope(scope);
22
124
  return {
23
125
  active: Boolean(data.active),
24
126
  code: String(data.code ?? ''),
25
127
  description: String(data.desc ?? ''),
26
128
  id: Number(data.id),
129
+ insertMethod: 'auto',
130
+ location: CODE_SNIPPETS_SCOPE_TO_LOCATION[scope] ?? defaultLocationForType(type),
27
131
  name: String(data.name ?? ''),
132
+ priority: resolvePriority(data.priority),
133
+ shortcodeAttributes: [],
28
134
  tags: Array.isArray(data.tags) ? data.tags : [],
29
- type: resolveType(data.type, String(data.code ?? '')),
135
+ type,
30
136
  };
31
137
  }
32
- toPayload(name, code, path, type) {
138
+ toPayload({ active, code, location, name, path, priority, tags, type }) {
33
139
  return {
34
- code: code.replace(/^<\?php\s*/i, ''),
140
+ active,
141
+ code: stripPhpOpeningTag(code),
35
142
  desc: `Imported from ${path}`,
36
143
  name,
37
- tags: ['cli-import'],
38
- type,
144
+ priority,
145
+ scope: scopeFromTypeAndLocation(type, location),
146
+ tags,
39
147
  };
40
148
  }
41
149
  }
150
+ // WPCode taxonomy term slugs (see wpcode_register_taxonomies() and the auto-insert location
151
+ // classes upstream). 'everywhere' | 'frontend_only' | 'admin_only' | 'on_demand' only apply to
152
+ // PHP snippets; 'site_wide_header' | 'site_wide_body' | 'site_wide_footer' apply to any type.
153
+ const WPCODE_PHP_ONLY_LOCATIONS = {
154
+ admin: 'admin_only',
155
+ everywhere: 'everywhere',
156
+ frontend: 'frontend_only',
157
+ once: 'on_demand',
158
+ };
159
+ const WPCODE_UNIVERSAL_LOCATIONS = {
160
+ body: 'site_wide_body',
161
+ footer: 'site_wide_footer',
162
+ header: 'site_wide_header',
163
+ };
164
+ const WPCODE_LOCATION_TO_CANONICAL = {
165
+ 'admin_only': 'admin',
166
+ everywhere: 'everywhere',
167
+ 'frontend_only': 'frontend',
168
+ 'on_demand': 'once',
169
+ 'site_wide_body': 'body',
170
+ 'site_wide_footer': 'footer',
171
+ 'site_wide_header': 'header',
172
+ };
173
+ function wpcodeLocationTerm(type, location) {
174
+ const universal = WPCODE_UNIVERSAL_LOCATIONS[location];
175
+ if (universal)
176
+ return universal;
177
+ if (type === 'php') {
178
+ const phpOnly = WPCODE_PHP_ONLY_LOCATIONS[location];
179
+ if (phpOnly)
180
+ return phpOnly;
181
+ }
182
+ const allowed = type === 'php' ? 'header, body, footer, everywhere, frontend, admin, once' : 'header, body, footer';
183
+ throw new Error(`WPCode does not support the "${location}" location for ${type} snippets. Use one of: ${allowed}.`);
184
+ }
42
185
  class WPCodePlugin {
43
- endpoint(siteUrl) {
44
- return `${siteUrl}/wp-json/loopress/v1/wpcode/snippets`;
186
+ endpointPath() {
187
+ return 'loopress/v1/wpcode/snippets';
45
188
  }
46
189
  fromRemote(data) {
190
+ const type = resolveType(data.type, String(data.code ?? ''));
47
191
  return {
48
192
  active: Boolean(data.active),
49
193
  code: String(data.code ?? ''),
50
194
  description: String(data.note ?? ''),
51
195
  id: Number(data.id),
196
+ insertMethod: data.insert_method === 'shortcode' ? 'shortcode' : 'auto',
197
+ location: WPCODE_LOCATION_TO_CANONICAL[String(data.location)] ?? defaultLocationForType(type),
52
198
  name: String(data.title ?? ''),
199
+ priority: resolvePriority(data.priority),
200
+ shortcodeAttributes: Array.isArray(data.shortcode_attributes) ? data.shortcode_attributes.map(String) : [],
53
201
  tags: Array.isArray(data.tags) ? data.tags : [],
54
- type: resolveType(data.type, String(data.code ?? '')),
202
+ type,
55
203
  };
56
204
  }
57
- toPayload(name, code, path, type) {
205
+ toPayload({ active, code, insertMethod, location, name, path, priority, shortcodeAttributes, tags, type, }) {
206
+ const placement = insertMethod === 'shortcode'
207
+ ? { 'shortcode_attributes': shortcodeAttributes }
208
+ : { location: wpcodeLocationTerm(type, location) };
58
209
  return {
59
- code,
210
+ active,
211
+ code: stripPhpOpeningTag(code),
60
212
  note: `Imported from ${path}`,
61
- tags: ['cli-import'],
213
+ priority,
214
+ tags,
62
215
  title: name,
63
216
  type,
217
+ ...placement,
218
+ 'insert_method': insertMethod,
64
219
  };
65
220
  }
66
221
  }