mage-remote-run 1.6.0 → 1.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.
- package/bin/mage-remote-run.js +11 -3
- package/lib/cli-options.js +3 -0
- package/lib/commands/plugins-actions.js +45 -9
- package/lib/commands/products-actions.js +85 -66
- package/lib/mcp.js +9 -7
- package/lib/plugin-loader.js +15 -6
- package/lib/utils.js +1 -1
- package/package.json +1 -1
package/bin/mage-remote-run.js
CHANGED
|
@@ -15,6 +15,7 @@ program
|
|
|
15
15
|
.name('mage-remote-run')
|
|
16
16
|
.description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
|
|
17
17
|
.version(pkg.version)
|
|
18
|
+
.option('--ignore-plugins', 'Skip loading configured plugins')
|
|
18
19
|
.configureHelp({
|
|
19
20
|
visibleCommands: (cmd) => {
|
|
20
21
|
const commands = cmd.commands.filter(c => !c._hidden);
|
|
@@ -49,6 +50,7 @@ import { eventBus, events } from '../lib/events.js';
|
|
|
49
50
|
import { createClient } from '../lib/api/factory.js';
|
|
50
51
|
import * as utils from '../lib/utils.js';
|
|
51
52
|
import * as commandHelper from '../lib/command-helper.js';
|
|
53
|
+
import { hasIgnorePluginsFlag } from '../lib/cli-options.js';
|
|
52
54
|
|
|
53
55
|
// Connection commands are registered dynamically via registerCommands
|
|
54
56
|
// But we need them registered early if we want them to show up in help even if config fails?
|
|
@@ -69,10 +71,14 @@ program.command('mcp [args...]')
|
|
|
69
71
|
if (args && args.length > 0) {
|
|
70
72
|
// console.error(chalk.yellow(`[mage-remote-run] Warning: Received extra arguments for mcp command: ${args.join(' ')}`));
|
|
71
73
|
}
|
|
72
|
-
await startMcpServer(
|
|
74
|
+
await startMcpServer({
|
|
75
|
+
...options,
|
|
76
|
+
ignorePlugins: program.opts().ignorePlugins,
|
|
77
|
+
});
|
|
73
78
|
});
|
|
74
79
|
|
|
75
80
|
const profile = await getActiveProfile();
|
|
81
|
+
const ignorePlugins = hasIgnorePluginsFlag(process.argv.slice(2));
|
|
76
82
|
|
|
77
83
|
// Load Plugins
|
|
78
84
|
// We construct an initial context.
|
|
@@ -94,8 +100,10 @@ const appContext = {
|
|
|
94
100
|
},
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
if (!ignorePlugins) {
|
|
104
|
+
const pluginLoader = new PluginLoader(appContext);
|
|
105
|
+
await pluginLoader.loadPlugins();
|
|
106
|
+
}
|
|
99
107
|
|
|
100
108
|
eventBus.emit(events.INIT, appContext);
|
|
101
109
|
import { registerVirtualCommands } from '../lib/commands/virtual.js';
|
|
@@ -3,6 +3,18 @@ import { loadConfig, saveConfig } from '../config.js';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { realpath } from 'node:fs/promises';
|
|
5
5
|
|
|
6
|
+
function getHomeDirectory() {
|
|
7
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function expandHomeDirectory(pluginRef) {
|
|
11
|
+
if (pluginRef.startsWith('~/') || pluginRef.startsWith('~\\')) {
|
|
12
|
+
return path.join(getHomeDirectory(), pluginRef.slice(2));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return pluginRef;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
function isFilesystemPath(pluginRef) {
|
|
7
19
|
const isScopedPackageName = /^@[^/\\]+\/[^/\\]+$/.test(pluginRef);
|
|
8
20
|
const hasPathSeparator = pluginRef.includes('/') || pluginRef.includes('\\');
|
|
@@ -23,24 +35,47 @@ function isFilesystemPath(pluginRef) {
|
|
|
23
35
|
async function resolvePluginReference(pluginRef) {
|
|
24
36
|
if (!isFilesystemPath(pluginRef)) return pluginRef;
|
|
25
37
|
|
|
38
|
+
if (pluginRef.startsWith('file:')) {
|
|
39
|
+
return realpath(new URL(pluginRef));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return realpath(expandHomeDirectory(pluginRef));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function normalizePluginReferenceForStorage(pluginRef) {
|
|
46
|
+
if (!isFilesystemPath(pluginRef) || pluginRef.startsWith('file:')) {
|
|
47
|
+
return resolvePluginReference(pluginRef);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolvedPluginRef = await resolvePluginReference(pluginRef);
|
|
51
|
+
|
|
26
52
|
if (pluginRef.startsWith('~/') || pluginRef.startsWith('~\\')) {
|
|
27
|
-
return
|
|
53
|
+
return pluginRef;
|
|
28
54
|
}
|
|
29
55
|
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
return resolvedPluginRef;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function findRegisteredPluginIndex(plugins, pluginRef) {
|
|
60
|
+
const resolvedPluginRef = await resolvePluginReference(pluginRef);
|
|
61
|
+
|
|
62
|
+
for (const [index, registeredPlugin] of plugins.entries()) {
|
|
63
|
+
const resolvedRegisteredPlugin = await resolvePluginReference(registeredPlugin);
|
|
64
|
+
if (resolvedRegisteredPlugin === resolvedPluginRef) {
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
return
|
|
69
|
+
return -1;
|
|
35
70
|
}
|
|
36
71
|
|
|
37
72
|
export async function registerPluginAction(packageName) {
|
|
38
73
|
try {
|
|
39
|
-
const pluginRef = await
|
|
74
|
+
const pluginRef = await normalizePluginReferenceForStorage(packageName);
|
|
40
75
|
const config = await loadConfig();
|
|
41
76
|
if (!config.plugins) config.plugins = [];
|
|
42
77
|
|
|
43
|
-
if (config.plugins
|
|
78
|
+
if ((await findRegisteredPluginIndex(config.plugins, pluginRef)) !== -1) {
|
|
44
79
|
console.log(chalk.yellow(`Plugin "${pluginRef}" is already registered.`));
|
|
45
80
|
return;
|
|
46
81
|
}
|
|
@@ -56,14 +91,15 @@ export async function registerPluginAction(packageName) {
|
|
|
56
91
|
|
|
57
92
|
export async function unregisterPluginAction(packageName) {
|
|
58
93
|
try {
|
|
59
|
-
const pluginRef = await
|
|
94
|
+
const pluginRef = await normalizePluginReferenceForStorage(packageName);
|
|
60
95
|
const config = await loadConfig();
|
|
61
|
-
|
|
96
|
+
const pluginIndex = config.plugins ? await findRegisteredPluginIndex(config.plugins, pluginRef) : -1;
|
|
97
|
+
if (pluginIndex === -1) {
|
|
62
98
|
console.log(chalk.yellow(`Plugin "${pluginRef}" is not registered.`));
|
|
63
99
|
return;
|
|
64
100
|
}
|
|
65
101
|
|
|
66
|
-
config.plugins = config.plugins.filter(
|
|
102
|
+
config.plugins = config.plugins.filter((_, index) => index !== pluginIndex);
|
|
67
103
|
await saveConfig(config);
|
|
68
104
|
console.log(chalk.green(`Plugin "${pluginRef}" successfully unregistered.`));
|
|
69
105
|
} catch (error) {
|
|
@@ -59,6 +59,85 @@ export async function listProductsAction(options) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function printProductGeneralInfo(data) {
|
|
63
|
+
console.log(chalk.bold('\nℹ️ General Information'));
|
|
64
|
+
console.log(` ${chalk.bold('ID:')} ${data.id}`);
|
|
65
|
+
console.log(` ${chalk.bold('SKU:')} ${data.sku}`);
|
|
66
|
+
console.log(` ${chalk.bold('Name:')} ${data.name}`);
|
|
67
|
+
console.log(` ${chalk.bold('Type:')} ${data.type_id}`);
|
|
68
|
+
console.log(` ${chalk.bold('Set ID:')} ${data.attribute_set_id}`);
|
|
69
|
+
console.log(` ${chalk.bold('Created At:')} ${data.created_at}`);
|
|
70
|
+
console.log(` ${chalk.bold('Updated At:')} ${data.updated_at}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printProductPricing(data) {
|
|
74
|
+
console.log(chalk.bold('\n💰 Pricing'));
|
|
75
|
+
console.log(` ${chalk.bold('Price:')} ${data.price}`);
|
|
76
|
+
const specialPrice = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'special_price');
|
|
77
|
+
if (specialPrice) {
|
|
78
|
+
console.log(` ${chalk.bold('Special Price:')} ${specialPrice.value}`);
|
|
79
|
+
}
|
|
80
|
+
if (data.tier_prices && data.tier_prices.length > 0) {
|
|
81
|
+
console.log(` ${chalk.bold('Tier Prices:')} ${data.tier_prices.length} defined`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printProductStock(data) {
|
|
86
|
+
if (data.extension_attributes && data.extension_attributes.stock_item) {
|
|
87
|
+
const stock = data.extension_attributes.stock_item;
|
|
88
|
+
console.log(chalk.bold('\n📦 Stock'));
|
|
89
|
+
console.log(` ${chalk.bold('In Stock:')} ${stock.is_in_stock ? chalk.green('Yes') : chalk.red('No')}`);
|
|
90
|
+
console.log(` ${chalk.bold('Quantity:')} ${stock.qty}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function printProductContent(data) {
|
|
95
|
+
const { convert } = await import('html-to-text');
|
|
96
|
+
const description = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'description');
|
|
97
|
+
const shortDescription = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'short_description');
|
|
98
|
+
|
|
99
|
+
if (description || shortDescription) {
|
|
100
|
+
console.log(chalk.bold('\n📝 Content'));
|
|
101
|
+
if (shortDescription) {
|
|
102
|
+
console.log(chalk.bold.underline('Short Description:'));
|
|
103
|
+
console.log(convert(shortDescription.value, { wordwrap: 80 }));
|
|
104
|
+
console.log('');
|
|
105
|
+
}
|
|
106
|
+
if (description) {
|
|
107
|
+
console.log(chalk.bold.underline('Description:'));
|
|
108
|
+
console.log(convert(description.value, { wordwrap: 80 }));
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function printProductAttributes(data) {
|
|
115
|
+
if (data.custom_attributes && data.custom_attributes.length > 0) {
|
|
116
|
+
const ignoredAttributes = ['description', 'short_description', 'special_price', 'category_ids', 'url_key'];
|
|
117
|
+
const visibleAttributes = data.custom_attributes.filter(a => !ignoredAttributes.includes(a.attribute_code));
|
|
118
|
+
|
|
119
|
+
if (visibleAttributes.length > 0) {
|
|
120
|
+
console.log(chalk.bold('\n📋 Additional Attributes'));
|
|
121
|
+
const attrRows = visibleAttributes
|
|
122
|
+
.filter(a => typeof a.value !== 'object')
|
|
123
|
+
.map(a => [a.attribute_code, a.value]);
|
|
124
|
+
|
|
125
|
+
if (attrRows.length > 0) {
|
|
126
|
+
printTable(['Attribute', 'Value'], attrRows);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printProductMedia(data) {
|
|
133
|
+
if (data.media_gallery_entries && data.media_gallery_entries.length > 0) {
|
|
134
|
+
console.log(chalk.bold('\n🖼️ Media'));
|
|
135
|
+
data.media_gallery_entries.forEach(entry => {
|
|
136
|
+
console.log(` [${entry.media_type}] ${entry.file} (${entry.label || 'No Label'})`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
62
141
|
export async function showProductAction(sku, options) {
|
|
63
142
|
try {
|
|
64
143
|
const client = await createClient();
|
|
@@ -83,72 +162,12 @@ export async function showProductAction(sku, options) {
|
|
|
83
162
|
console.log(chalk.bold.blue('\n📦 Product Information'));
|
|
84
163
|
console.log(chalk.gray('━'.repeat(60)));
|
|
85
164
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(` ${chalk.bold('Created At:')} ${data.created_at}`);
|
|
93
|
-
console.log(` ${chalk.bold('Updated At:')} ${data.updated_at}`);
|
|
94
|
-
|
|
95
|
-
console.log(chalk.bold('\n💰 Pricing'));
|
|
96
|
-
console.log(` ${chalk.bold('Price:')} ${data.price}`);
|
|
97
|
-
const specialPrice = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'special_price');
|
|
98
|
-
if (specialPrice) {
|
|
99
|
-
console.log(` ${chalk.bold('Special Price:')} ${specialPrice.value}`);
|
|
100
|
-
}
|
|
101
|
-
if (data.tier_prices && data.tier_prices.length > 0) {
|
|
102
|
-
console.log(` ${chalk.bold('Tier Prices:')} ${data.tier_prices.length} defined`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (data.extension_attributes && data.extension_attributes.stock_item) {
|
|
106
|
-
const stock = data.extension_attributes.stock_item;
|
|
107
|
-
console.log(chalk.bold('\n📦 Stock'));
|
|
108
|
-
console.log(` ${chalk.bold('In Stock:')} ${stock.is_in_stock ? chalk.green('Yes') : chalk.red('No')}`);
|
|
109
|
-
console.log(` ${chalk.bold('Quantity:')} ${stock.qty}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const { convert } = await import('html-to-text');
|
|
113
|
-
const description = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'description');
|
|
114
|
-
const shortDescription = data.custom_attributes && data.custom_attributes.find(a => a.attribute_code === 'short_description');
|
|
115
|
-
|
|
116
|
-
if (description || shortDescription) {
|
|
117
|
-
console.log(chalk.bold('\n📝 Content'));
|
|
118
|
-
if (shortDescription) {
|
|
119
|
-
console.log(chalk.bold.underline('Short Description:'));
|
|
120
|
-
console.log(convert(shortDescription.value, { wordwrap: 80 }));
|
|
121
|
-
console.log('');
|
|
122
|
-
}
|
|
123
|
-
if (description) {
|
|
124
|
-
console.log(chalk.bold.underline('Description:'));
|
|
125
|
-
console.log(convert(description.value, { wordwrap: 80 }));
|
|
126
|
-
console.log('');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (data.custom_attributes && data.custom_attributes.length > 0) {
|
|
131
|
-
const ignoredAttributes = ['description', 'short_description', 'special_price', 'category_ids', 'url_key'];
|
|
132
|
-
const visibleAttributes = data.custom_attributes.filter(a => !ignoredAttributes.includes(a.attribute_code));
|
|
133
|
-
|
|
134
|
-
if (visibleAttributes.length > 0) {
|
|
135
|
-
console.log(chalk.bold('\n📋 Additional Attributes'));
|
|
136
|
-
const attrRows = visibleAttributes
|
|
137
|
-
.filter(a => typeof a.value !== 'object')
|
|
138
|
-
.map(a => [a.attribute_code, a.value]);
|
|
139
|
-
|
|
140
|
-
if (attrRows.length > 0) {
|
|
141
|
-
printTable(['Attribute', 'Value'], attrRows);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (data.media_gallery_entries && data.media_gallery_entries.length > 0) {
|
|
147
|
-
console.log(chalk.bold('\n🖼️ Media'));
|
|
148
|
-
data.media_gallery_entries.forEach(entry => {
|
|
149
|
-
console.log(` [${entry.media_type}] ${entry.file} (${entry.label || 'No Label'})`);
|
|
150
|
-
});
|
|
151
|
-
}
|
|
165
|
+
printProductGeneralInfo(data);
|
|
166
|
+
printProductPricing(data);
|
|
167
|
+
printProductStock(data);
|
|
168
|
+
await printProductContent(data);
|
|
169
|
+
printProductAttributes(data);
|
|
170
|
+
printProductMedia(data);
|
|
152
171
|
|
|
153
172
|
console.log(chalk.gray('━'.repeat(60)));
|
|
154
173
|
} catch (e) {
|
package/lib/mcp.js
CHANGED
|
@@ -139,7 +139,7 @@ export async function startMcpServer(options) {
|
|
|
139
139
|
);
|
|
140
140
|
|
|
141
141
|
// 1. Setup a dynamic program to discovery commands
|
|
142
|
-
const program = await setupProgramAsync();
|
|
142
|
+
const program = await setupProgramAsync(options);
|
|
143
143
|
|
|
144
144
|
const server = new McpServer({
|
|
145
145
|
name: "mage-remote-run",
|
|
@@ -362,7 +362,7 @@ function registerTools(server, program, options = {}) {
|
|
|
362
362
|
description,
|
|
363
363
|
zodShape,
|
|
364
364
|
async (args) => {
|
|
365
|
-
return await executeCommand(cmd, args, segments);
|
|
365
|
+
return await executeCommand(cmd, args, segments, options);
|
|
366
366
|
}
|
|
367
367
|
);
|
|
368
368
|
count++;
|
|
@@ -373,7 +373,7 @@ function registerTools(server, program, options = {}) {
|
|
|
373
373
|
|
|
374
374
|
// Re-register all commands on a fresh program instance
|
|
375
375
|
// We export this logic so we can reuse it
|
|
376
|
-
async function setupProgramAsync() {
|
|
376
|
+
async function setupProgramAsync(options = {}) {
|
|
377
377
|
const program = new Command();
|
|
378
378
|
|
|
379
379
|
// Silence output for the main program instance to avoid double printing during parsing
|
|
@@ -394,8 +394,10 @@ async function setupProgramAsync() {
|
|
|
394
394
|
events
|
|
395
395
|
};
|
|
396
396
|
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
if (!options.ignorePlugins) {
|
|
398
|
+
const pluginLoader = new PluginLoader(appContext);
|
|
399
|
+
await pluginLoader.loadPlugins();
|
|
400
|
+
}
|
|
399
401
|
|
|
400
402
|
localEventBus.emit(events.INIT, appContext);
|
|
401
403
|
|
|
@@ -412,7 +414,7 @@ async function setupProgramAsync() {
|
|
|
412
414
|
return program;
|
|
413
415
|
}
|
|
414
416
|
|
|
415
|
-
async function executeCommand(cmdDefinition, args, commandSegments) {
|
|
417
|
+
async function executeCommand(cmdDefinition, args, commandSegments, options = {}) {
|
|
416
418
|
// Intercept Console
|
|
417
419
|
let output = '';
|
|
418
420
|
const originalLog = console.log;
|
|
@@ -428,7 +430,7 @@ async function executeCommand(cmdDefinition, args, commandSegments) {
|
|
|
428
430
|
console.error = logInterceptor;
|
|
429
431
|
|
|
430
432
|
try {
|
|
431
|
-
const program = await setupProgramAsync();
|
|
433
|
+
const program = await setupProgramAsync(options);
|
|
432
434
|
|
|
433
435
|
// Construct argv
|
|
434
436
|
// We need to build [node, script, command, subcommand, ..., args, options]
|
package/lib/plugin-loader.js
CHANGED
|
@@ -8,6 +8,14 @@ import { loadConfig } from './config.js';
|
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
|
|
11
|
+
function expandHomeDirectory(pluginRef) {
|
|
12
|
+
if (pluginRef.startsWith('~/') || pluginRef.startsWith('~\\')) {
|
|
13
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '', pluginRef.slice(2));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return pluginRef;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
export class PluginLoader {
|
|
12
20
|
constructor(appContext) {
|
|
13
21
|
this.appContext = appContext;
|
|
@@ -19,7 +27,7 @@ export class PluginLoader {
|
|
|
19
27
|
// If config.plugins is missing, we default to empty array
|
|
20
28
|
const plugins = config.plugins || [];
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
await Promise.all(plugins.map(async (pluginName) => {
|
|
23
31
|
try {
|
|
24
32
|
await this.loadPlugin(pluginName);
|
|
25
33
|
} catch (e) {
|
|
@@ -28,19 +36,20 @@ export class PluginLoader {
|
|
|
28
36
|
console.error(e);
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
|
-
}
|
|
39
|
+
}));
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
async loadPlugin(pluginName) {
|
|
43
|
+
const resolvedPluginName = expandHomeDirectory(pluginName);
|
|
35
44
|
let pluginPath;
|
|
36
45
|
|
|
37
46
|
// 1. Try local node_modules
|
|
38
47
|
try {
|
|
39
|
-
pluginPath = require.resolve(
|
|
48
|
+
pluginPath = require.resolve(resolvedPluginName);
|
|
40
49
|
} catch (e) {
|
|
41
50
|
// 2. Try global node_modules (npm)
|
|
42
51
|
try {
|
|
43
|
-
const globalNpmPath = path.join(globalDirs.npm.packages,
|
|
52
|
+
const globalNpmPath = path.join(globalDirs.npm.packages, resolvedPluginName);
|
|
44
53
|
const npmExists = await fs.promises.access(globalNpmPath).then(() => true).catch(() => false);
|
|
45
54
|
if (npmExists) {
|
|
46
55
|
const pkgJsonPath = path.join(globalNpmPath, 'package.json');
|
|
@@ -55,7 +64,7 @@ export class PluginLoader {
|
|
|
55
64
|
}
|
|
56
65
|
} else {
|
|
57
66
|
// 3. Try global node_modules (yarn)
|
|
58
|
-
const globalYarnPath = path.join(globalDirs.yarn.packages,
|
|
67
|
+
const globalYarnPath = path.join(globalDirs.yarn.packages, resolvedPluginName);
|
|
59
68
|
const yarnExists = await fs.promises.access(globalYarnPath).then(() => true).catch(() => false);
|
|
60
69
|
if (yarnExists) {
|
|
61
70
|
const pkgJsonPath = path.join(globalYarnPath, 'package.json');
|
|
@@ -77,7 +86,7 @@ export class PluginLoader {
|
|
|
77
86
|
|
|
78
87
|
if (!pluginPath) {
|
|
79
88
|
throw new Error(`Could not resolve plugin '${pluginName}' locally or globally.`);
|
|
80
|
-
|
|
89
|
+
}
|
|
81
90
|
|
|
82
91
|
// Import using file URL for absolute paths in ESM
|
|
83
92
|
// Windows paths need to be converted to file URLs
|
package/lib/utils.js
CHANGED
|
@@ -178,7 +178,7 @@ export function buildSortCriteria(options) {
|
|
|
178
178
|
for (const s of options.sort) {
|
|
179
179
|
const parts = s.split(':');
|
|
180
180
|
const field = parts[0];
|
|
181
|
-
const direction = parts.length > 1 ? parts[1].toUpperCase() : 'ASC';
|
|
181
|
+
const direction = (parts.length > 1 && parts[1]) ? parts[1].toUpperCase() : 'ASC';
|
|
182
182
|
addSort(field, direction);
|
|
183
183
|
}
|
|
184
184
|
} else if (options.sortBy) {
|