@loopress/cli 0.4.0 → 0.6.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.
@@ -1,5 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import got from 'got';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { LoopressCommand } from '../../lib/base.js';
4
5
  import { readLocalConfig, writeLocalConfig } from '../../utils/loopress-config.js';
5
6
  const WP_ORG_API = 'https://api.wordpress.org/plugins/info/1.2/';
@@ -24,26 +25,46 @@ export async function resolvePluginVersion(slug, version) {
24
25
  throw new Error(`Plugin "${slug}" not found on WordPress.org.`);
25
26
  return info.version;
26
27
  }
27
- export default class Require extends LoopressCommand {
28
+ export default class Add extends LoopressCommand {
28
29
  static args = {
29
- slug: Args.string({ description: 'Plugin slug (WordPress.org)', required: true }),
30
+ slug: Args.string({ description: 'Plugin slug (WordPress.org) or Composer package (vendor/package)', required: true }),
30
31
  version: Args.string({ description: 'Version to pin (default: latest)' }),
31
32
  };
32
- static description = 'Add a plugin to loopress.json, resolving its latest version from WordPress.org';
33
+ static description = 'Add a plugin to loopress.json (WordPress.org) or run composer require (vendor/package)';
33
34
  static examples = [
34
- '$ lps plugins require woocommerce',
35
- '$ lps plugins require woocommerce 8.9.1',
36
- '$ lps plugins require contact-form-7 --dry-run',
35
+ '$ lps plugin add woocommerce',
36
+ '$ lps plugin add woocommerce 8.9.1',
37
+ '$ lps plugin add wpackagist-plugin/advanced-custom-fields',
38
+ '$ lps plugin add contact-form-7 --dry-run',
37
39
  ];
38
40
  static flags = {
39
41
  ...LoopressCommand.baseFlags,
40
42
  'dry-run': Flags.boolean({ char: 'd', description: 'Show what would be written without making changes' }),
41
43
  };
42
44
  async run() {
43
- const { args, flags } = await this.parse(Require);
45
+ const { args, flags } = await this.parse(Add);
44
46
  const dryRun = flags['dry-run'];
45
47
  const { slug } = args;
46
48
  const requestedVersion = args.version ?? 'latest';
49
+ if (slug.includes('/')) {
50
+ await this.requireComposerPackage(slug, requestedVersion, dryRun);
51
+ return;
52
+ }
53
+ await this.requireWpOrgPlugin(slug, requestedVersion, dryRun);
54
+ }
55
+ async requireComposerPackage(pkg, version, dryRun) {
56
+ const composerArg = version === 'latest' ? pkg : `${pkg}:${version}`;
57
+ this.log(`Running: composer require ${composerArg}`);
58
+ if (dryRun) {
59
+ this.log(`[dry-run] Would run: composer require ${composerArg}`);
60
+ return;
61
+ }
62
+ const result = spawnSync('composer', ['require', composerArg], { stdio: 'inherit' });
63
+ if (result.status !== 0) {
64
+ this.error('composer require failed. Make sure Composer is installed and accessible.');
65
+ }
66
+ }
67
+ async requireWpOrgPlugin(slug, requestedVersion, dryRun) {
47
68
  this.log(`Resolving ${slug}@${requestedVersion}...`);
48
69
  let resolvedVersion;
49
70
  try {
@@ -56,7 +77,7 @@ export default class Require extends LoopressCommand {
56
77
  const localConfig = await readLocalConfig();
57
78
  const existing = localConfig.plugins ?? {};
58
79
  if (existing[slug] === resolvedVersion) {
59
- this.log(`${slug}@${resolvedVersion} is already in loopress.json nothing to do.`);
80
+ this.log(`${slug}@${resolvedVersion} is already in loopress.json, nothing to do.`);
60
81
  return;
61
82
  }
62
83
  const updated = existing[slug] !== undefined;
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import got from 'got';
3
3
  import { LoopressCommand } from '../../lib/base.js';
4
+ import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
4
5
  import { readLocalConfig, writeLocalConfig } from '../../utils/loopress-config.js';
5
6
  import { mergePluginManifest } from '../../utils/plugins.js';
6
7
  export default class Pull extends LoopressCommand {
@@ -17,7 +18,15 @@ export default class Pull extends LoopressCommand {
17
18
  this.log(`Pulling plugins from ${url}`);
18
19
  const headers = await this.buildAuthHeaders();
19
20
  const installed = await got.get(`${url}/wp-json/loopress/v1/plugins`, { headers }).json();
20
- const incoming = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
21
+ const composerJson = await readComposerJson();
22
+ const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
23
+ const incoming = Object.fromEntries(installed.filter((p) => !composerSlugs.includes(p.slug)).map((p) => [p.slug, p.version]));
24
+ if (composerSlugs.length > 0) {
25
+ const found = installed.filter((p) => composerSlugs.includes(p.slug)).map((p) => p.slug);
26
+ if (found.length > 0) {
27
+ this.log(`Skipping ${found.length} Composer-managed ${found.length === 1 ? 'plugin' : 'plugins'}: ${found.join(', ')}`);
28
+ }
29
+ }
21
30
  const localConfig = await readLocalConfig();
22
31
  const { added, merged, updated } = mergePluginManifest(localConfig.plugins ?? {}, incoming);
23
32
  if (dryRun) {
@@ -1,5 +1,5 @@
1
- import { LoopressCommand } from '../../lib/base.js';
2
- export default class Push extends LoopressCommand {
1
+ import { PushCommand } from '../../lib/push-command.js';
2
+ export default class Push extends PushCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
@@ -1,29 +1,39 @@
1
1
  import { confirm } from '@inquirer/prompts';
2
2
  import { Flags } from '@oclif/core';
3
3
  import got from 'got';
4
- import { LoopressCommand } from '../../lib/base.js';
4
+ import { PushCommand } from '../../lib/push-command.js';
5
+ import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
5
6
  import { readLocalConfig } from '../../utils/loopress-config.js';
6
7
  import { diffPlugins } from '../../utils/plugins.js';
7
- export default class Push extends LoopressCommand {
8
+ export default class Push extends PushCommand {
8
9
  static description = 'Sync plugins on WordPress to match loopress.json';
9
10
  static examples = ['$ lps plugins push', '$ lps plugins push --dry-run'];
10
11
  static flags = {
11
- ...LoopressCommand.baseFlags,
12
+ ...PushCommand.baseFlags,
12
13
  'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
13
14
  };
14
15
  async run() {
15
16
  const { flags } = await this.parse(Push);
16
17
  const dryRun = flags['dry-run'];
18
+ this.dryRun = dryRun;
17
19
  const { url } = this.siteConfig;
18
20
  const localConfig = await readLocalConfig();
19
21
  const manifest = localConfig.plugins;
20
22
  if (!manifest || Object.keys(manifest).length === 0) {
21
23
  this.error('No plugins found in loopress.json. Run `lps plugins pull` first.');
22
24
  }
25
+ const composerJson = await readComposerJson();
26
+ const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
27
+ const filteredManifest = Object.fromEntries(Object.entries(manifest).filter(([slug]) => !composerSlugs.includes(slug)));
28
+ const skipped = composerSlugs.filter((slug) => slug in manifest);
29
+ if (skipped.length > 0) {
30
+ this.log(`Skipping ${skipped.length} Composer-managed ${skipped.length === 1 ? 'plugin' : 'plugins'}: ${skipped.join(', ')}`);
31
+ this.log('Run `lps composer push` to deploy them.');
32
+ }
23
33
  this.log(`Pushing plugins to ${url}`);
24
34
  const headers = await this.buildAuthHeaders();
25
35
  const installed = await got.get(`${url}/wp-json/loopress/v1/plugins`, { headers }).json();
26
- const { drifted, toActivate, toInstall } = diffPlugins(manifest, installed);
36
+ const { drifted, toActivate, toInstall } = diffPlugins(filteredManifest, installed);
27
37
  if (toInstall.length === 0 && toActivate.length === 0 && drifted.length === 0) {
28
38
  this.log('Everything is already in sync.');
29
39
  return;
@@ -96,6 +106,7 @@ export default class Push extends LoopressCommand {
96
106
  }
97
107
  await this.activatePlugin(url, headers, action.slug);
98
108
  }
109
+ await this.recordSuccess();
99
110
  }
100
111
  async activatePlugin(url, headers, slug) {
101
112
  try {
@@ -17,9 +17,9 @@ export default class Config extends Command {
17
17
  });
18
18
  const envChoice = await select({
19
19
  choices: [
20
- { name: 'production', value: 'production' },
20
+ { name: 'local', value: 'local' },
21
21
  { name: 'staging', value: 'staging' },
22
- { name: 'development', value: 'development' },
22
+ { name: 'production', value: 'production' },
23
23
  { name: 'Custom…', value: '__custom__' },
24
24
  ],
25
25
  message: 'Environment',
@@ -4,7 +4,7 @@ export default class List extends LoopressCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
- plugin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -14,8 +14,7 @@ export default class List extends LoopressCommand {
14
14
  json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
15
15
  plugin: Flags.string({
16
16
  char: 'p',
17
- default: 'code-snippets',
18
- description: 'WordPress snippet plugin to target',
17
+ description: 'WordPress snippet plugin to target (overrides loopress.json)',
19
18
  options: ['code-snippets', 'wpcode'],
20
19
  }),
21
20
  };
@@ -23,8 +22,9 @@ export default class List extends LoopressCommand {
23
22
  const { flags } = await this.parse(List);
24
23
  const { json, plugin } = flags;
25
24
  const { url } = this.siteConfig;
25
+ const resolvedPlugin = await this.resolveSnippetPlugin(plugin);
26
26
  try {
27
- const adapter = getSnippetPlugin(plugin);
27
+ const adapter = getSnippetPlugin(resolvedPlugin);
28
28
  const endpoint = adapter.endpoint(url);
29
29
  const headers = await this.buildAuthHeaders();
30
30
  const remoteList = await got.get(endpoint, { headers }).json();
@@ -1,4 +1,7 @@
1
1
  import { LoopressCommand } from '../../lib/base.js';
2
+ import { NormalizedSnippet } from '../../utils/snippet-plugin.js';
3
+ export declare function buildSnippetFile(snippet: NormalizedSnippet): string;
4
+ export declare function buildMetaFile(snippet: NormalizedSnippet): string;
2
5
  export default class Pull extends LoopressCommand {
3
6
  static args: {
4
7
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
@@ -7,7 +10,7 @@ export default class Pull extends LoopressCommand {
7
10
  static examples: string[];
8
11
  static flags: {
9
12
  dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- plugin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
14
  password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
15
  url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
16
  user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -10,39 +10,24 @@ const EXTENSIONS = {
10
10
  php: 'php',
11
11
  text: 'txt',
12
12
  };
13
- const sanitize = (value) => value.replaceAll(/\s*\n\s*/g, ' ').trim();
14
- function buildMetaLines(snippet) {
15
- return [
16
- `id: ${snippet.id}`,
17
- `name: ${sanitize(snippet.name)}`,
18
- ...(snippet.description ? [`description: ${sanitize(snippet.description)}`] : []),
19
- `type: ${snippet.type}`,
20
- ...(snippet.tags.length > 0 ? [`tags: ${snippet.tags.map((t) => sanitize(t)).join(', ')}`] : []),
21
- `active: ${snippet.active}`,
22
- ];
23
- }
24
- function buildSnippetFile(snippet) {
25
- const meta = buildMetaLines(snippet);
26
- switch (snippet.type) {
27
- case 'css':
28
- case 'js': {
29
- const header = ['/**', ...meta.map((l) => ` * ${l}`), ' */'].join('\n');
30
- return `${header}\n\n${snippet.code}`;
31
- }
32
- case 'html': {
33
- const header = ['<!--', ...meta.map((l) => ` ${l}`), '-->'].join('\n');
34
- return `${header}\n\n${snippet.code}`;
35
- }
36
- case 'text': {
37
- return snippet.code;
38
- }
39
- case 'php':
40
- default: {
41
- const header = ['<?php', '/**', ...meta.map((l) => ` * ${l}`), ' */'].join('\n');
42
- const body = snippet.code.replace(/^<\?php\s*/i, '');
43
- return `${header}\n\n${body}`;
44
- }
13
+ export function buildSnippetFile(snippet) {
14
+ if (snippet.type === 'php' && !snippet.code.trimStart().startsWith('<?')) {
15
+ return `<?php\n\n${snippet.code}`;
45
16
  }
17
+ return snippet.code;
18
+ }
19
+ export function buildMetaFile(snippet) {
20
+ const meta = {
21
+ id: snippet.id,
22
+ name: snippet.name,
23
+ type: snippet.type,
24
+ active: snippet.active,
25
+ };
26
+ if (snippet.description)
27
+ meta.description = snippet.description;
28
+ if (snippet.tags.length > 0)
29
+ meta.tags = snippet.tags;
30
+ return JSON.stringify(meta, null, 2) + '\n';
46
31
  }
47
32
  export default class Pull extends LoopressCommand {
48
33
  static args = {
@@ -60,8 +45,7 @@ export default class Pull extends LoopressCommand {
60
45
  dryRun: Flags.boolean({ char: 'd', description: 'Dry run - show what would happen without making changes' }),
61
46
  plugin: Flags.string({
62
47
  char: 'p',
63
- default: 'code-snippets',
64
- description: 'WordPress snippet plugin to target',
48
+ description: 'WordPress snippet plugin to target (overrides loopress.json)',
65
49
  options: ['code-snippets', 'wpcode'],
66
50
  }),
67
51
  };
@@ -70,11 +54,12 @@ export default class Pull extends LoopressCommand {
70
54
  const { dryRun, plugin } = flags;
71
55
  const { url } = this.siteConfig;
72
56
  const path = await this.resolveSnippetsPath(args.path);
73
- this.log(`📥 Pulling snippets from ${url} via ${plugin}`);
57
+ const resolvedPlugin = await this.resolveSnippetPlugin(plugin);
58
+ this.log(`📥 Pulling snippets from ${url} via ${resolvedPlugin}`);
74
59
  this.log(`📂 From snippet path: ${path}`);
75
60
  this.log(`🔄 Dry run: ${dryRun ? 'yes' : 'no'}`);
76
61
  try {
77
- const adapter = getSnippetPlugin(plugin);
62
+ const adapter = getSnippetPlugin(resolvedPlugin);
78
63
  const endpoint = adapter.endpoint(url);
79
64
  const headers = await this.buildAuthHeaders();
80
65
  const remoteList = await got.get(endpoint, { headers }).json();
@@ -94,8 +79,12 @@ export default class Pull extends LoopressCommand {
94
79
  continue;
95
80
  }
96
81
  const ext = EXTENSIONS[snippet.type];
97
- const filePath = `${path}/${slugify(snippet.name, { lower: true, strict: true })}.${ext}`;
82
+ const slug = slugify(snippet.name, { lower: true, strict: true });
83
+ const base = `${snippet.id}-${slug}`;
84
+ const filePath = `${path}/${base}.${ext}`;
85
+ const metaPath = `${path}/${base}.json`;
98
86
  await fs.writeFile(filePath, buildSnippetFile(snippet));
87
+ await fs.writeFile(metaPath, buildMetaFile(snippet));
99
88
  count++;
100
89
  this.log(`✅ Pulled: ${snippet.name}`);
101
90
  }
@@ -1,5 +1,5 @@
1
- import { LoopressCommand } from '../../lib/base.js';
2
- export default class Push extends LoopressCommand {
1
+ import { PushCommand } from '../../lib/push-command.js';
2
+ export default class Push extends PushCommand {
3
3
  static args: {
4
4
  path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
5
  };
@@ -7,14 +7,13 @@ export default class Push extends LoopressCommand {
7
7
  static examples: string[];
8
8
  static flags: {
9
9
  dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- plugin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  };
15
15
  run(): Promise<void>;
16
- private injectIdIntoFile;
16
+ private injectIdIntoMeta;
17
17
  private loadSnippets;
18
- private parseMetaFromContent;
19
18
  private pushSnippet;
20
19
  }
@@ -1,8 +1,8 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import got from 'got';
3
- import { LoopressCommand } from '../../lib/base.js';
3
+ import { PushCommand } from '../../lib/push-command.js';
4
4
  import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
5
- export default class Push extends LoopressCommand {
5
+ export default class Push extends PushCommand {
6
6
  static args = {
7
7
  path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
8
8
  };
@@ -14,68 +14,86 @@ export default class Push extends LoopressCommand {
14
14
  '$ lps snippets push --plugin wpcode',
15
15
  ];
16
16
  static flags = {
17
- ...LoopressCommand.baseFlags,
17
+ ...PushCommand.baseFlags,
18
18
  dryRun: Flags.boolean({ char: 'd', description: 'Dry run - show what would happen without making changes' }),
19
19
  plugin: Flags.string({
20
20
  char: 'p',
21
- default: 'code-snippets',
22
- description: 'WordPress snippet plugin to target',
21
+ description: 'WordPress snippet plugin to target (overrides loopress.json)',
23
22
  options: ['code-snippets', 'wpcode'],
24
23
  }),
25
24
  };
26
25
  async run() {
27
26
  const { args, flags } = await this.parse(Push);
28
27
  const { dryRun, plugin } = flags;
28
+ this.dryRun = dryRun;
29
29
  const { url } = this.siteConfig;
30
30
  const path = await this.resolveSnippetsPath(args.path);
31
- this.log(`🚀 Pushing snippets to ${url} via ${plugin}`);
31
+ const resolvedPlugin = await this.resolveSnippetPlugin(plugin);
32
+ this.log(`🚀 Pushing snippets to ${url} via ${resolvedPlugin}`);
32
33
  this.log(`📂 From snippet path: ${path}`);
33
34
  this.log(`🔄 Dry run: ${dryRun ? 'yes' : 'no'}`);
35
+ let snippets = [];
34
36
  try {
35
- const snippets = await this.loadSnippets(path);
37
+ snippets = await this.loadSnippets(path);
36
38
  this.log(`✅ Found ${snippets.length} snippets to push`);
37
39
  const headers = await this.buildAuthHeaders();
38
- const adapter = getSnippetPlugin(plugin);
40
+ const adapter = getSnippetPlugin(resolvedPlugin);
39
41
  for (const snippet of snippets) {
40
- await this.pushSnippet(snippet, url, headers, dryRun, adapter);
42
+ await this.pushSnippet(snippet, { adapter, dryRun, headers, url });
41
43
  }
44
+ await this.recordSuccess();
42
45
  this.log('🎉 All snippets pushed successfully!');
43
46
  }
44
47
  catch (error) {
45
48
  this.error(error.message);
46
49
  }
47
50
  }
48
- async injectIdIntoFile(filePath, content, id) {
51
+ async injectIdIntoMeta(filePath, id) {
49
52
  const fs = await import('node:fs/promises');
50
- let updated;
51
- if (content.includes('/**')) {
52
- updated = content.replace('/**', `/**\n * id: ${id}`);
53
- }
54
- else if (content.includes('<!--')) {
55
- updated = content.replace('<!--', `<!--\n id: ${id}`);
53
+ const metaPath = filePath.replace(/\.[^.]+$/, '.json');
54
+ let meta = {};
55
+ try {
56
+ const existing = await fs.readFile(metaPath, 'utf8');
57
+ meta = JSON.parse(existing);
56
58
  }
57
- else {
58
- return;
59
+ catch (error) {
60
+ if (error.code !== 'ENOENT')
61
+ throw error;
59
62
  }
60
- await fs.writeFile(filePath, updated);
63
+ meta.id = id;
64
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
61
65
  }
62
66
  async loadSnippets(path) {
63
67
  const fs = await import('node:fs/promises');
64
68
  const snippets = [];
69
+ const SNIPPET_EXTENSIONS = new Set(['.css', '.html', '.js', '.php', '.txt']);
65
70
  try {
66
71
  const files = await fs.readdir(path);
67
72
  for (const file of files) {
68
- if (file.endsWith('.php')) {
69
- const filePath = `${path}/${file}`;
70
- const content = await fs.readFile(filePath, 'utf8');
71
- const meta = this.parseMetaFromContent(content);
72
- snippets.push({
73
- code: content,
74
- id: meta.id,
75
- name: meta.name ?? file.replace('.php', ''),
76
- path: filePath,
77
- });
73
+ const ext = file.slice(file.lastIndexOf('.'));
74
+ if (!SNIPPET_EXTENSIONS.has(ext))
75
+ continue;
76
+ const filePath = `${path}/${file}`;
77
+ const metaPath = filePath.slice(0, filePath.lastIndexOf('.')) + '.json';
78
+ const content = await fs.readFile(filePath, 'utf8');
79
+ let id;
80
+ let name;
81
+ try {
82
+ const metaContent = await fs.readFile(metaPath, 'utf8');
83
+ const meta = JSON.parse(metaContent);
84
+ id = meta.id ? Number(meta.id) : undefined;
85
+ name = meta.name ? String(meta.name) : undefined;
86
+ }
87
+ catch (error) {
88
+ if (error.code !== 'ENOENT')
89
+ throw error;
78
90
  }
91
+ snippets.push({
92
+ code: content,
93
+ id,
94
+ name: name ?? file.slice(0, file.lastIndexOf('.')),
95
+ path: filePath,
96
+ });
79
97
  }
80
98
  }
81
99
  catch (error) {
@@ -83,15 +101,8 @@ export default class Push extends LoopressCommand {
83
101
  }
84
102
  return snippets;
85
103
  }
86
- parseMetaFromContent(content) {
87
- const idMatch = content.match(/[\s*]*id:\s*(\d+)/);
88
- const nameMatch = content.match(/[\s*]*name:\s*(.+)/);
89
- return {
90
- id: idMatch ? Number(idMatch[1]) : undefined,
91
- name: nameMatch ? nameMatch[1].trim() : undefined,
92
- };
93
- }
94
- async pushSnippet(snippet, url, headers, dryRun, adapter) {
104
+ async pushSnippet(snippet, ctx) {
105
+ const { adapter, dryRun, headers, url } = ctx;
95
106
  if (dryRun) {
96
107
  this.log(`📝 [DRY RUN] Would push snippet: ${snippet.name}`);
97
108
  this.log(`📄 Code preview: ${snippet.code.slice(0, 100)}...`);
@@ -109,7 +120,7 @@ export default class Push extends LoopressCommand {
109
120
  this.log(`➕ Creating new snippet: ${snippet.name}`);
110
121
  const response = await got.post(endpoint, { headers, json: payload }).json();
111
122
  const created = adapter.fromRemote(response);
112
- await this.injectIdIntoFile(snippet.path, snippet.code, created.id);
123
+ await this.injectIdIntoMeta(snippet.path, created.id);
113
124
  this.log(`✅ Created: ${snippet.name} (id: ${created.id})`);
114
125
  }
115
126
  catch (error) {
@@ -30,10 +30,10 @@ export class AuthManager {
30
30
  }
31
31
  }
32
32
  getAuthFilePath() {
33
- return join(this.homeDir, '.lps', 'auth.json');
33
+ return join(this.homeDir, '.loopress', 'auth.json');
34
34
  }
35
35
  setAuth(auth) {
36
- const dir = join(this.homeDir, '.lps');
36
+ const dir = join(this.homeDir, '.loopress');
37
37
  if (!existsSync(dir))
38
38
  mkdirSync(dir, { recursive: true });
39
39
  const filePath = this.getAuthFilePath();
@@ -14,13 +14,13 @@ export class ProjectConfigManager {
14
14
  return ProjectConfigManager.instance;
15
15
  }
16
16
  ensureConfigDir() {
17
- const dir = join(this.homeDir, '.lps');
17
+ const dir = join(this.homeDir, '.loopress');
18
18
  if (!existsSync(dir)) {
19
19
  mkdirSync(dir, { recursive: true });
20
20
  }
21
21
  }
22
22
  getConfigFilePath() {
23
- return join(this.homeDir, '.lps', 'config.json');
23
+ return join(this.homeDir, '.loopress', 'config.json');
24
24
  }
25
25
  getCurrentEnv() {
26
26
  const project = this.getCurrentProject();
@@ -9,6 +9,6 @@ export declare abstract class LoopressCommand extends Command {
9
9
  protected siteConfig: EnvironmentConfig;
10
10
  buildAuthHeaders(): Promise<Record<string, string>>;
11
11
  init(): Promise<void>;
12
+ protected resolveSnippetPlugin(flag?: string): Promise<'code-snippets' | 'wpcode'>;
12
13
  protected resolveSnippetsPath(override?: string): Promise<string>;
13
- protected resolveStylesPath(override?: string): Promise<string>;
14
14
  }
package/dist/lib/base.js CHANGED
@@ -27,6 +27,18 @@ export class LoopressCommand extends Command {
27
27
  }
28
28
  async init() {
29
29
  await super.init();
30
+ const localConfig = await readLocalConfig();
31
+ if (localConfig.projectId) {
32
+ const project = configManager.getProject(localConfig.projectId);
33
+ if (!project) {
34
+ this.error(`Project "${localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
35
+ }
36
+ if (!project.currentEnv || !project.environments[project.currentEnv]) {
37
+ this.error(`Project "${localConfig.projectId}" has no active environment. Run \`lps project config\` to configure one.`);
38
+ }
39
+ this.siteConfig = project.environments[project.currentEnv];
40
+ return;
41
+ }
30
42
  const env = configManager.getCurrentEnv();
31
43
  if (env) {
32
44
  this.siteConfig = env;
@@ -34,16 +46,16 @@ export class LoopressCommand extends Command {
34
46
  }
35
47
  this.error('No environment configured. Run `lps project config` first.');
36
48
  }
37
- async resolveSnippetsPath(override) {
38
- if (override)
39
- return override;
49
+ async resolveSnippetPlugin(flag) {
50
+ if (flag)
51
+ return flag;
40
52
  const config = await readLocalConfig();
41
- return join(config.rootDir ?? '.', config.snippets ?? 'snippets');
53
+ return config.snippetPlugin ?? 'wpcode';
42
54
  }
43
- async resolveStylesPath(override) {
55
+ async resolveSnippetsPath(override) {
44
56
  if (override)
45
57
  return override;
46
58
  const config = await readLocalConfig();
47
- return join(config.rootDir ?? '.', config.styles ?? 'styles');
59
+ return join(config.rootDir ?? '.', config.snippetsDir ?? 'snippets');
48
60
  }
49
61
  }
@@ -0,0 +1,7 @@
1
+ import { LoopressCommand } from './base.js';
2
+ export declare abstract class PushCommand extends LoopressCommand {
3
+ protected dryRun: boolean;
4
+ catch(err: Error): Promise<void>;
5
+ protected recordDeployment(status: 'failure' | 'success'): Promise<void>;
6
+ protected recordSuccess(): Promise<void>;
7
+ }
@@ -0,0 +1,32 @@
1
+ import got from 'got';
2
+ import { authManager } from '../config/auth.manager.js';
3
+ import { LoopressCommand } from './base.js';
4
+ const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
5
+ export class PushCommand extends LoopressCommand {
6
+ dryRun = false;
7
+ async catch(err) {
8
+ if (!this.dryRun && this.siteConfig) {
9
+ await this.recordDeployment('failure');
10
+ }
11
+ return super.catch(err);
12
+ }
13
+ async recordDeployment(status) {
14
+ const token = process.env.LPS_TOKEN ?? authManager.getAuth()?.token ?? null;
15
+ if (!token)
16
+ return;
17
+ try {
18
+ await got.post(`${API_URL}/deployments`, {
19
+ headers: { Authorization: `Bearer ${token}` },
20
+ json: { status, url: this.siteConfig.url },
21
+ timeout: { request: 3000 },
22
+ });
23
+ }
24
+ catch {
25
+ // non-blocking: recording must never interrupt the push flow
26
+ }
27
+ }
28
+ async recordSuccess() {
29
+ if (!this.dryRun)
30
+ await this.recordDeployment('success');
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ export interface ComposerJson {
2
+ require?: Record<string, string>;
3
+ 'require-dev'?: Record<string, string>;
4
+ }
5
+ export declare function readComposerJson(): Promise<ComposerJson | null>;
6
+ export declare function readComposerLock(): Promise<null | string>;
7
+ export declare function getComposerManagedSlugs(composerJson: ComposerJson): string[];