@loopress/cli 0.9.0 → 0.11.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 (37) hide show
  1. package/README.md +41 -35
  2. package/dist/commands/init.js +24 -10
  3. package/dist/commands/project/config.js +1 -1
  4. package/dist/commands/project/remove.js +3 -2
  5. package/dist/commands/project/switch.d.ts +1 -2
  6. package/dist/commands/project/switch.js +28 -34
  7. package/dist/commands/project/sync.d.ts +13 -0
  8. package/dist/commands/project/sync.js +210 -0
  9. package/dist/commands/snippet/list.d.ts +0 -1
  10. package/dist/commands/snippet/list.js +4 -7
  11. package/dist/commands/snippet/pull.d.ts +1 -2
  12. package/dist/commands/snippet/pull.js +6 -10
  13. package/dist/commands/snippet/push.d.ts +1 -1
  14. package/dist/commands/snippet/push.js +37 -16
  15. package/dist/config/project-config.manager.d.ts +3 -1
  16. package/dist/config/project-config.manager.js +27 -3
  17. package/dist/help.js +1 -1
  18. package/dist/lib/api-client.d.ts +14 -0
  19. package/dist/lib/api-client.js +46 -0
  20. package/dist/lib/base.d.ts +0 -1
  21. package/dist/lib/base.js +0 -5
  22. package/dist/lib/push-command.js +1 -1
  23. package/dist/lib/wp-client.d.ts +1 -0
  24. package/dist/lib/wp-client.js +4 -0
  25. package/dist/types/global-config.generated.d.ts +8 -0
  26. package/dist/types/project-config.generated.d.ts +0 -4
  27. package/dist/types/snippet.d.ts +1 -1
  28. package/dist/utils/{snippet-plugin.d.ts → snippet-format.d.ts} +3 -19
  29. package/dist/utils/snippet-format.js +57 -0
  30. package/oclif.manifest.json +28 -43
  31. package/package.json +7 -1
  32. package/schemas/global-config.schema.json +91 -0
  33. package/schemas/project-config.schema.json +42 -0
  34. package/schemas/snippet.schema.json +61 -0
  35. package/dist/utils/snippet-plugin-flag.d.ts +0 -3
  36. package/dist/utils/snippet-plugin-flag.js +0 -8
  37. package/dist/utils/snippet-plugin.js +0 -232
@@ -1,19 +1,16 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { LoopressCommand } from '../../lib/base.js';
3
- import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
4
- import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
3
+ import { normalizeSnippet, SNIPPETS_ENDPOINT } from '../../utils/snippet-format.js';
5
4
  export default class List extends LoopressCommand {
6
5
  static description = 'List snippets from WordPress';
7
- static examples = ['$ lps snippet list', '$ lps snippet list --plugin wpcode'];
6
+ static examples = ['$ lps snippet list'];
8
7
  static flags = {
9
8
  json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
10
- ...snippetPluginFlag,
11
9
  };
12
10
  async run() {
13
11
  const { flags } = await this.parse(List);
14
- const adapter = getSnippetPlugin(this.resolveSnippetPlugin(flags.plugin));
15
- const remoteList = await this.wp.get(adapter.endpointPath());
16
- const snippets = remoteList.map((r) => adapter.fromRemote(r));
12
+ const remoteList = await this.wp.get(SNIPPETS_ENDPOINT);
13
+ const snippets = remoteList.map((r) => normalizeSnippet(r));
17
14
  if (flags.json) {
18
15
  this.log(JSON.stringify(snippets, null, 2));
19
16
  return;
@@ -1,5 +1,5 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
- import { NormalizedSnippet } from '../../utils/snippet-plugin.js';
2
+ import { NormalizedSnippet } from '../../utils/snippet-format.js';
3
3
  export declare function buildSnippetFile(snippet: NormalizedSnippet): string;
4
4
  export declare function buildMetaFile(snippet: NormalizedSnippet): string;
5
5
  export default class Pull extends LoopressCommand {
@@ -9,7 +9,6 @@ export default class Pull extends LoopressCommand {
9
9
  static description: string;
10
10
  static examples: string[];
11
11
  static flags: {
12
- plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
13
  };
15
14
  run(): Promise<void>;
@@ -4,8 +4,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import slugify from 'slugify';
6
6
  import { LoopressCommand } from '../../lib/base.js';
7
- import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
8
- import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
7
+ import { normalizeSnippet, SNIPPETS_ENDPOINT } from '../../utils/snippet-format.js';
9
8
  const EXTENSIONS = {
10
9
  css: 'css',
11
10
  html: 'html',
@@ -44,21 +43,18 @@ export default class Pull extends LoopressCommand {
44
43
  path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
45
44
  };
46
45
  static description = 'Pull snippets from WordPress';
47
- static examples = ['$ lps snippet pull', '$ lps snippet pull --path ./snippets', '$ lps snippet pull --plugin wpcode'];
46
+ static examples = ['$ lps snippet pull', '$ lps snippet pull --path ./snippets'];
48
47
  static flags = {
49
48
  ...LoopressCommand.dryRunFlag,
50
- ...snippetPluginFlag,
51
49
  };
52
50
  async run() {
53
- const { args, flags } = await this.parse(Pull);
51
+ const { args } = await this.parse(Pull);
54
52
  const { url } = this.siteConfig;
55
53
  const path = this.resolveSnippetsPath(args.path);
56
- const resolvedPlugin = this.resolveSnippetPlugin(flags.plugin);
57
- this.log(`Pulling snippets from ${url} via ${resolvedPlugin}`);
54
+ this.log(`Pulling snippets from ${url}`);
58
55
  this.log(`Snippets path: ${path}`);
59
- const adapter = getSnippetPlugin(resolvedPlugin);
60
- const remoteList = await this.wp.get(adapter.endpointPath());
61
- const snippets = remoteList.map((r) => adapter.fromRemote(r));
56
+ const remoteList = await this.wp.get(SNIPPETS_ENDPOINT);
57
+ const snippets = remoteList.map((r) => normalizeSnippet(r));
62
58
  if (this.dryRun) {
63
59
  this.log(`[dry-run] Would pull ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to ${path}`);
64
60
  return;
@@ -6,7 +6,6 @@ export default class Push extends PushCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
9
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
10
  };
12
11
  private failedCount;
@@ -14,4 +13,5 @@ export default class Push extends PushCommand {
14
13
  private ensureCanonicalFilename;
15
14
  private loadSnippets;
16
15
  private pushSnippet;
16
+ private toPayload;
17
17
  }
@@ -4,8 +4,8 @@ import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
4
  import { basename, dirname, extname, join } from 'node:path';
5
5
  import slugify from 'slugify';
6
6
  import { PushCommand } from '../../lib/push-command.js';
7
- import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
8
- import { defaultLocationForType, getSnippetPlugin, parseInsertMethod, parseLocation, parseType, } from '../../utils/snippet-plugin.js';
7
+ import { isNotFoundError } from '../../lib/wp-client.js';
8
+ import { defaultLocationForType, normalizeSnippet, parseInsertMethod, parseLocation, parseType, SNIPPETS_ENDPOINT, stripPhpOpeningTag, } from '../../utils/snippet-format.js';
9
9
  const TYPE_BY_EXTENSION = {
10
10
  '.css': 'css',
11
11
  '.html': 'html',
@@ -18,24 +18,21 @@ export default class Push extends PushCommand {
18
18
  path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
19
19
  };
20
20
  static description = 'Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
21
- static examples = ['$ lps snippet push', '$ lps snippet push --path ./snippets', '$ lps snippet push --plugin wpcode'];
21
+ static examples = ['$ lps snippet push', '$ lps snippet push --path ./snippets'];
22
22
  static flags = {
23
23
  ...PushCommand.dryRunFlag,
24
- ...snippetPluginFlag,
25
24
  };
26
25
  failedCount = 0;
27
26
  async run() {
28
- const { args, flags } = await this.parse(Push);
27
+ const { args } = await this.parse(Push);
29
28
  const { url } = this.siteConfig;
30
29
  const path = this.resolveSnippetsPath(args.path);
31
- const resolvedPlugin = this.resolveSnippetPlugin(flags.plugin);
32
- this.log(`Pushing snippets to ${url} via ${resolvedPlugin}`);
30
+ this.log(`Pushing snippets to ${url}`);
33
31
  this.log(`Snippets path: ${path}`);
34
32
  const snippets = await this.loadSnippets(path);
35
33
  this.log(`Found ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to push`);
36
- const adapter = getSnippetPlugin(resolvedPlugin);
37
34
  await new Listr(snippets.map((snippet) => ({
38
- task: async (_ctx, task) => this.pushSnippet(snippet, adapter, task),
35
+ task: async (_ctx, task) => this.pushSnippet(snippet, task),
39
36
  title: `Push ${snippet.name}`,
40
37
  })), { concurrent: false, exitOnError: false }).run();
41
38
  if (this.failedCount > 0) {
@@ -141,22 +138,32 @@ export default class Push extends PushCommand {
141
138
  // Throwing on failure (rather than returning a boolean) is what lets Listr mark the task as
142
139
  // failed (red cross) instead of completed; `exitOnError: false` on the task list still lets
143
140
  // sibling snippets push regardless.
144
- async pushSnippet(snippet, adapter, task) {
141
+ async pushSnippet(snippet, task) {
145
142
  if (this.dryRun) {
146
143
  if (task)
147
144
  task.output = `[dry-run] Would push: ${snippet.name}`;
148
145
  return;
149
146
  }
150
- const endpointPath = adapter.endpointPath();
151
147
  try {
152
- const payload = adapter.toPayload(snippet);
148
+ const payload = this.toPayload(snippet);
153
149
  if (snippet.id) {
154
- await this.wp.put(`${endpointPath}/${snippet.id}`, payload);
155
- await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
150
+ try {
151
+ await this.wp.put(`${SNIPPETS_ENDPOINT}/${snippet.id}`, payload);
152
+ await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
153
+ }
154
+ catch (error) {
155
+ // The id recorded locally doesn't exist on this site (e.g. a fresh install): create it
156
+ // instead of failing, and adopt whatever id the site assigns.
157
+ if (!isNotFoundError(error))
158
+ throw error;
159
+ const response = await this.wp.post(SNIPPETS_ENDPOINT, payload);
160
+ const created = normalizeSnippet(response);
161
+ await this.ensureCanonicalFilename(snippet, created.id, created.name);
162
+ }
156
163
  }
157
164
  else {
158
- const response = await this.wp.post(endpointPath, payload);
159
- const created = adapter.fromRemote(response);
165
+ const response = await this.wp.post(SNIPPETS_ENDPOINT, payload);
166
+ const created = normalizeSnippet(response);
160
167
  await this.ensureCanonicalFilename(snippet, created.id, created.name);
161
168
  }
162
169
  if (task)
@@ -172,4 +179,18 @@ export default class Push extends PushCommand {
172
179
  throw error;
173
180
  }
174
181
  }
182
+ toPayload(snippet) {
183
+ return {
184
+ active: snippet.active,
185
+ code: stripPhpOpeningTag(snippet.code),
186
+ description: `Imported from ${snippet.path}`,
187
+ insertMethod: snippet.insertMethod,
188
+ location: snippet.location,
189
+ name: snippet.name,
190
+ priority: snippet.priority,
191
+ shortcodeAttributes: snippet.shortcodeAttributes,
192
+ tags: snippet.tags,
193
+ type: snippet.type,
194
+ };
195
+ }
175
196
  }
@@ -2,7 +2,7 @@ import { EnvironmentConfig, LoopressConfig, ProjectConfig } from '../types/confi
2
2
  export declare class ProjectConfigManager {
3
3
  private readonly homeDir;
4
4
  constructor(homeDir?: string);
5
- createProjectId(): string;
5
+ createProjectId(name: string): string;
6
6
  ensureConfigDir(): void;
7
7
  getConfigFilePath(): string;
8
8
  getCurrentEnv(): EnvironmentConfig | null;
@@ -23,7 +23,9 @@ export declare class ProjectConfigManager {
23
23
  removeProject(id: string): void;
24
24
  setCurrent(projectId: string, envName: string): void;
25
25
  setEnvironment(projectId: string, envName: string, env: EnvironmentConfig): void;
26
+ setEnvironmentApiId(projectId: string, envName: string, apiEnvironmentId: string): void;
26
27
  setProject(id: string, project: ProjectConfig): void;
28
+ setProjectApiId(id: string, apiProjectId: string): void;
27
29
  writeConfig(config: LoopressConfig): void;
28
30
  private isProjectConfig;
29
31
  private sanitizeConfig;
@@ -1,15 +1,23 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { existsSync, mkdirSync } from 'node:fs';
3
2
  import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
4
+ import slugify from 'slugify';
5
5
  import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
6
6
  export class ProjectConfigManager {
7
7
  homeDir;
8
8
  constructor(homeDir = homedir()) {
9
9
  this.homeDir = homeDir;
10
10
  }
11
- createProjectId() {
12
- return randomUUID();
11
+ createProjectId(name) {
12
+ const config = this.readConfig();
13
+ const base = slugify(name, { lower: true, strict: true }) || 'project';
14
+ let id = base;
15
+ let suffix = 2;
16
+ while (config.projects[id]) {
17
+ id = `${base}-${suffix}`;
18
+ suffix++;
19
+ }
20
+ return id;
13
21
  }
14
22
  ensureConfigDir() {
15
23
  const dir = join(this.homeDir, '.loopress');
@@ -113,6 +121,14 @@ export class ProjectConfigManager {
113
121
  config.currentProject = { env: envName, id: projectId };
114
122
  this.writeConfig(config);
115
123
  }
124
+ setEnvironmentApiId(projectId, envName, apiEnvironmentId) {
125
+ const config = this.readConfig();
126
+ const env = config.projects[projectId]?.environments[envName];
127
+ if (!env)
128
+ return;
129
+ env.apiEnvironmentId = apiEnvironmentId;
130
+ this.writeConfig(config);
131
+ }
116
132
  setProject(id, project) {
117
133
  const config = this.readConfig();
118
134
  config.projects[id] = project;
@@ -123,6 +139,14 @@ export class ProjectConfigManager {
123
139
  }
124
140
  this.writeConfig(config);
125
141
  }
142
+ setProjectApiId(id, apiProjectId) {
143
+ const config = this.readConfig();
144
+ const project = config.projects[id];
145
+ if (!project)
146
+ return;
147
+ project.apiProjectId = apiProjectId;
148
+ this.writeConfig(config);
149
+ }
126
150
  writeConfig(config) {
127
151
  writeJsonFileAtomic(this.getConfigFilePath(), config);
128
152
  }
package/dist/help.js CHANGED
@@ -7,7 +7,7 @@ const BANNER = `
7
7
  @@@@ ++++ ▗▖ ▗▄▖ ▗▄▖ ▗▄▄▖ ▗▄▄▖ ▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖
8
8
  @@@@ ++++ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ ▐▌
9
9
  @@@@ ++++ ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▛▀▘ ▐▛▀▚▖▐▛▀▀▘ ▝▀▚▖ ▝▀▚▖
10
- @@@@ ++++ ▐▙▄▄▖▝▚▄▞▘▝▚▄▞▘▐▌ ▐▌ ▐▌▐▙▄▄▖▗▄▄▞▘▗▄▄▞▘
10
+ @@@@ ++++ ▐▙▄▄▖▝▚▄▞▘▝▚▄▞▘▐▌ ▐▌ ▐▌▐▙▄▄▖▗▄▄▞▘▗▄▄▞▘ (BETA)
11
11
  @@@@ ++++
12
12
  @@@@ ++++
13
13
  @@@@@@@@@@@*++
@@ -0,0 +1,14 @@
1
+ export declare const API_URL: string;
2
+ /**
3
+ * HTTP client for the Loopress cloud API (projects, environments, credentials).
4
+ * Authenticated with the console token obtained via `lps login`.
5
+ */
6
+ export declare class ApiClient {
7
+ private readonly baseUrl;
8
+ private readonly client;
9
+ constructor(token: string, baseUrl?: string);
10
+ get<T = unknown>(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
+ }
@@ -0,0 +1,46 @@
1
+ import got from 'got';
2
+ export const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
3
+ /**
4
+ * HTTP client for the Loopress cloud API (projects, environments, credentials).
5
+ * Authenticated with the console token obtained via `lps login`.
6
+ */
7
+ export class ApiClient {
8
+ baseUrl;
9
+ client;
10
+ constructor(token, baseUrl = API_URL) {
11
+ this.baseUrl = baseUrl;
12
+ this.client = got.extend({
13
+ headers: { Authorization: `Bearer ${token}` },
14
+ prefixUrl: baseUrl,
15
+ });
16
+ }
17
+ async get(path) {
18
+ return this.request('get', path);
19
+ }
20
+ async post(path, json) {
21
+ return this.request('post', path, json);
22
+ }
23
+ async put(path, json) {
24
+ return this.request('put', path, json);
25
+ }
26
+ async request(method, path, json) {
27
+ try {
28
+ const response = await this.client(path, { json, method });
29
+ return (response.body ? JSON.parse(response.body) : undefined);
30
+ }
31
+ catch (error) {
32
+ throw new Error(formatApiError(error, `${this.baseUrl}/${path}`), { cause: error });
33
+ }
34
+ }
35
+ }
36
+ function formatApiError(error, url) {
37
+ const err = error;
38
+ const status = err.response?.statusCode;
39
+ if (status === 401) {
40
+ return `Not logged in or session expired on ${url}. Run \`lps login\` again.`;
41
+ }
42
+ if (status === 403) {
43
+ return `Request rejected (${status}) on ${url}: ${err.response?.body ?? err.message}`;
44
+ }
45
+ return err.message ?? String(error);
46
+ }
@@ -13,7 +13,6 @@ export declare abstract class LoopressCommand extends Command {
13
13
  protected get rootDir(): string;
14
14
  protected get wp(): WpClient;
15
15
  init(): Promise<void>;
16
- protected resolveSnippetPlugin(flag?: string): 'code-snippets' | 'wpcode';
17
16
  protected resolveSnippetsPath(override?: string): string;
18
17
  private resolveEnvironment;
19
18
  }
package/dist/lib/base.js CHANGED
@@ -35,11 +35,6 @@ export class LoopressCommand extends Command {
35
35
  this.localConfig = await readLocalConfig();
36
36
  this.siteConfig = this.resolveEnvironment();
37
37
  }
38
- resolveSnippetPlugin(flag) {
39
- if (flag)
40
- return flag;
41
- return this.localConfig.snippetPlugin ?? 'wpcode';
42
- }
43
38
  resolveSnippetsPath(override) {
44
39
  if (override)
45
40
  return override;
@@ -10,7 +10,7 @@ export class PushCommand extends LoopressCommand {
10
10
  return super.catch(err);
11
11
  }
12
12
  async recordDeployment(status) {
13
- const token = process.env.LPS_TOKEN ?? authManager.getAuth()?.token ?? null;
13
+ const token = process.env.LOOPRESS_TOKEN ?? authManager.getAuth()?.token ?? null;
14
14
  if (!token)
15
15
  return;
16
16
  try {
@@ -12,4 +12,5 @@ export declare class WpClient {
12
12
  put<T = unknown>(path: string, json?: Record<string, unknown>): Promise<T>;
13
13
  private request;
14
14
  }
15
+ export declare function isNotFoundError(error: unknown): boolean;
15
16
  export declare function formatWpError(error: unknown, url: string): string;
@@ -34,6 +34,10 @@ export class WpClient {
34
34
  }
35
35
  }
36
36
  }
37
+ export function isNotFoundError(error) {
38
+ const cause = error?.cause;
39
+ return cause?.response?.statusCode === 404;
40
+ }
37
41
  export function formatWpError(error, url) {
38
42
  const err = error;
39
43
  const status = err.response?.statusCode;
@@ -28,6 +28,10 @@ export interface ProjectConfig {
28
28
  * ISO timestamp of when the project was added.
29
29
  */
30
30
  addedAt: string;
31
+ /**
32
+ * Id of the matching project on the Loopress API, once synced via `lps project sync`.
33
+ */
34
+ apiProjectId?: string;
31
35
  /**
32
36
  * Environments for this project, keyed by environment name.
33
37
  */
@@ -44,6 +48,10 @@ export interface EnvironmentConfig {
44
48
  * ISO timestamp of when the environment was added.
45
49
  */
46
50
  addedAt: string;
51
+ /**
52
+ * Id of the matching environment on the Loopress API, once synced via `lps project sync`.
53
+ */
54
+ apiEnvironmentId?: string;
47
55
  /**
48
56
  * Environment name (e.g. "production", "staging").
49
57
  */
@@ -15,10 +15,6 @@ export interface LoopressProjectConfiguration {
15
15
  * Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config.
16
16
  */
17
17
  projectId?: string;
18
- /**
19
- * WordPress snippet plugin to use for pull and push commands.
20
- */
21
- snippetPlugin?: 'wpcode' | 'code-snippets';
22
18
  /**
23
19
  * Pinned plugin versions. Keys are WordPress.org plugin slugs, values are version constraints.
24
20
  */
@@ -1,4 +1,4 @@
1
- import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-plugin.js';
1
+ import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-format.js';
2
2
  export interface Snippet {
3
3
  active: boolean;
4
4
  code: string;
@@ -1,4 +1,4 @@
1
- export type PluginName = 'code-snippets' | 'wpcode';
1
+ export declare const SNIPPETS_ENDPOINT = "loopress/v1/snippets";
2
2
  export type SnippetType = 'css' | 'html' | 'js' | 'php' | 'text';
3
3
  export type SnippetInsertMethod = 'auto' | 'shortcode';
4
4
  export type SnippetLocation = 'admin' | 'body' | 'everywhere' | 'footer' | 'frontend' | 'header' | 'once';
@@ -19,21 +19,5 @@ export declare function parseType(raw: unknown): null | SnippetType;
19
19
  export declare function parseLocation(raw: unknown): null | SnippetLocation;
20
20
  export declare function parseInsertMethod(raw: unknown): null | SnippetInsertMethod;
21
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
- }
34
- export interface SnippetPlugin {
35
- endpointPath(): string;
36
- fromRemote(data: Record<string, unknown>): NormalizedSnippet;
37
- toPayload(snippet: SnippetPayloadInput): Record<string, unknown>;
38
- }
39
- export declare function getSnippetPlugin(name: PluginName): SnippetPlugin;
22
+ export declare function stripPhpOpeningTag(code: string): string;
23
+ export declare function normalizeSnippet(data: Record<string, unknown>): NormalizedSnippet;
@@ -0,0 +1,57 @@
1
+ export const SNIPPETS_ENDPOINT = 'loopress/v1/snippets';
2
+ export function parseType(raw) {
3
+ const valid = ['css', 'html', 'js', 'php', 'text'];
4
+ const value = String(raw ?? '').toLowerCase();
5
+ return valid.includes(value) ? value : null;
6
+ }
7
+ const VALID_LOCATIONS = new Set(['admin', 'body', 'everywhere', 'footer', 'frontend', 'header', 'once']);
8
+ export function parseLocation(raw) {
9
+ const value = String(raw ?? '').toLowerCase();
10
+ return VALID_LOCATIONS.has(value) ? value : null;
11
+ }
12
+ export function parseInsertMethod(raw) {
13
+ return raw === 'auto' || raw === 'shortcode' ? raw : null;
14
+ }
15
+ // The sensible default placement for a freshly pushed snippet that doesn't specify a location.
16
+ export function defaultLocationForType(type) {
17
+ switch (type) {
18
+ case 'css': {
19
+ return 'header';
20
+ }
21
+ case 'html':
22
+ case 'js':
23
+ case 'text': {
24
+ return 'footer';
25
+ }
26
+ case 'php': {
27
+ return 'everywhere';
28
+ }
29
+ }
30
+ }
31
+ function resolvePriority(raw) {
32
+ const value = Number(raw);
33
+ return Number.isFinite(value) ? value : 10;
34
+ }
35
+ // Snippet files on disk keep the <?php opening tag so they're valid, syntax-highlighted PHP files.
36
+ // The WordPress plugin stores just the executable body, so it's stripped before push and restored
37
+ // on pull (see buildSnippetFile in commands/snippet/pull.ts).
38
+ export function stripPhpOpeningTag(code) {
39
+ return code.replace(/^<\?php\s*/i, '');
40
+ }
41
+ // Defensive coercion for whatever JSON the `loopress/v1/snippets` endpoint returns, in case a
42
+ // field is missing or malformed server-side.
43
+ export function normalizeSnippet(data) {
44
+ return {
45
+ active: Boolean(data.active),
46
+ code: String(data.code ?? ''),
47
+ description: String(data.description ?? ''),
48
+ id: Number(data.id),
49
+ insertMethod: parseInsertMethod(data.insertMethod) ?? 'auto',
50
+ location: parseLocation(data.location) ?? defaultLocationForType(parseType(data.type) ?? 'php'),
51
+ name: String(data.name ?? ''),
52
+ priority: resolvePriority(data.priority),
53
+ shortcodeAttributes: Array.isArray(data.shortcodeAttributes) ? data.shortcodeAttributes.map(String) : [],
54
+ tags: Array.isArray(data.tags) ? data.tags : [],
55
+ type: parseType(data.type) ?? 'php',
56
+ };
57
+ }
@@ -407,13 +407,36 @@
407
407
  "switch.js"
408
408
  ]
409
409
  },
410
+ "project:sync": {
411
+ "aliases": [],
412
+ "args": {},
413
+ "description": "Sync locally configured projects and environments with your Loopress account",
414
+ "examples": [
415
+ "$ lps project sync"
416
+ ],
417
+ "flags": {},
418
+ "hasDynamicHelp": false,
419
+ "hiddenAliases": [],
420
+ "id": "project:sync",
421
+ "pluginAlias": "@loopress/cli",
422
+ "pluginName": "@loopress/cli",
423
+ "pluginType": "core",
424
+ "strict": true,
425
+ "enableJsonFlag": false,
426
+ "isESM": true,
427
+ "relativePath": [
428
+ "dist",
429
+ "commands",
430
+ "project",
431
+ "sync.js"
432
+ ]
433
+ },
410
434
  "snippet:list": {
411
435
  "aliases": [],
412
436
  "args": {},
413
437
  "description": "List snippets from WordPress",
414
438
  "examples": [
415
- "$ lps snippet list",
416
- "$ lps snippet list --plugin wpcode"
439
+ "$ lps snippet list"
417
440
  ],
418
441
  "flags": {
419
442
  "json": {
@@ -422,18 +445,6 @@
422
445
  "name": "json",
423
446
  "allowNo": false,
424
447
  "type": "boolean"
425
- },
426
- "plugin": {
427
- "char": "p",
428
- "description": "WordPress snippet plugin to target (overrides loopress.json)",
429
- "name": "plugin",
430
- "hasDynamicHelp": false,
431
- "multiple": false,
432
- "options": [
433
- "code-snippets",
434
- "wpcode"
435
- ],
436
- "type": "option"
437
448
  }
438
449
  },
439
450
  "hasDynamicHelp": false,
@@ -471,8 +482,7 @@
471
482
  "description": "Pull snippets from WordPress",
472
483
  "examples": [
473
484
  "$ lps snippet pull",
474
- "$ lps snippet pull --path ./snippets",
475
- "$ lps snippet pull --plugin wpcode"
485
+ "$ lps snippet pull --path ./snippets"
476
486
  ],
477
487
  "flags": {
478
488
  "dry-run": {
@@ -481,18 +491,6 @@
481
491
  "name": "dry-run",
482
492
  "allowNo": false,
483
493
  "type": "boolean"
484
- },
485
- "plugin": {
486
- "char": "p",
487
- "description": "WordPress snippet plugin to target (overrides loopress.json)",
488
- "name": "plugin",
489
- "hasDynamicHelp": false,
490
- "multiple": false,
491
- "options": [
492
- "code-snippets",
493
- "wpcode"
494
- ],
495
- "type": "option"
496
494
  }
497
495
  },
498
496
  "hasDynamicHelp": false,
@@ -530,8 +528,7 @@
530
528
  "description": "Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.",
531
529
  "examples": [
532
530
  "$ lps snippet push",
533
- "$ lps snippet push --path ./snippets",
534
- "$ lps snippet push --plugin wpcode"
531
+ "$ lps snippet push --path ./snippets"
535
532
  ],
536
533
  "flags": {
537
534
  "dry-run": {
@@ -540,18 +537,6 @@
540
537
  "name": "dry-run",
541
538
  "allowNo": false,
542
539
  "type": "boolean"
543
- },
544
- "plugin": {
545
- "char": "p",
546
- "description": "WordPress snippet plugin to target (overrides loopress.json)",
547
- "name": "plugin",
548
- "hasDynamicHelp": false,
549
- "multiple": false,
550
- "options": [
551
- "code-snippets",
552
- "wpcode"
553
- ],
554
- "type": "option"
555
540
  }
556
541
  },
557
542
  "hasDynamicHelp": false,
@@ -570,5 +555,5 @@
570
555
  ]
571
556
  }
572
557
  },
573
- "version": "0.9.0"
558
+ "version": "0.11.0"
574
559
  }