mage-remote-run 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/api-specs/2.4.8/swagger-paas.json +70180 -0
- package/api-specs/2.4.8/swagger-saas.json +58076 -0
- package/bin/mage-remote-run.js +61 -0
- package/lib/api/client.js +22 -0
- package/lib/api/factory.js +27 -0
- package/lib/api/paas.js +103 -0
- package/lib/api/saas.js +97 -0
- package/lib/api/spec-loader.js +23 -0
- package/lib/commands/connections.js +214 -0
- package/lib/commands/customers.js +196 -0
- package/lib/commands/eav.js +49 -0
- package/lib/commands/orders.js +247 -0
- package/lib/commands/products.js +133 -0
- package/lib/commands/stores.js +113 -0
- package/lib/commands/tax.js +46 -0
- package/lib/commands/websites.js +76 -0
- package/lib/config.js +54 -0
- package/lib/prompts.js +99 -0
- package/lib/utils.js +18 -0
- package/package.json +43 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { loadConfig } from '../lib/config.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('mage-remote-run')
|
|
15
|
+
.description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
|
|
16
|
+
.version(pkg.version);
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import { registerWebsitesCommands } from '../lib/commands/websites.js';
|
|
21
|
+
import { registerStoresCommands } from '../lib/commands/stores.js';
|
|
22
|
+
import { registerConnectionCommands } from '../lib/commands/connections.js';
|
|
23
|
+
import { registerCustomersCommands } from '../lib/commands/customers.js';
|
|
24
|
+
import { registerOrdersCommands } from '../lib/commands/orders.js';
|
|
25
|
+
import { registerEavCommands } from '../lib/commands/eav.js';
|
|
26
|
+
import { registerProductsCommands } from '../lib/commands/products.js';
|
|
27
|
+
import { registerTaxCommands } from '../lib/commands/tax.js';
|
|
28
|
+
|
|
29
|
+
registerConnectionCommands(program);
|
|
30
|
+
registerWebsitesCommands(program);
|
|
31
|
+
registerStoresCommands(program);
|
|
32
|
+
registerCustomersCommands(program);
|
|
33
|
+
registerOrdersCommands(program);
|
|
34
|
+
registerEavCommands(program);
|
|
35
|
+
registerProductsCommands(program);
|
|
36
|
+
registerTaxCommands(program);
|
|
37
|
+
|
|
38
|
+
// Check for first run (no profiles configured and no arguments or just help)
|
|
39
|
+
// We need to check args length.
|
|
40
|
+
// node script.js -> length 2.
|
|
41
|
+
// node script.js command -> length 3.
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const config = await loadConfig();
|
|
44
|
+
const hasProfiles = Object.keys(config.profiles || {}).length > 0;
|
|
45
|
+
|
|
46
|
+
if (!hasProfiles && args.length === 0) {
|
|
47
|
+
console.log(chalk.bold.blue('Welcome to mage-remote-run! š'));
|
|
48
|
+
console.log(chalk.gray('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce'));
|
|
49
|
+
console.log(chalk.gray('It looks like you haven\'t configured any connections yet.'));
|
|
50
|
+
console.log(chalk.gray('Let\'s set up your first connection now.\n'));
|
|
51
|
+
|
|
52
|
+
// Trigger the interactive add command directly
|
|
53
|
+
// We can simulate running the 'connection add' command
|
|
54
|
+
// But since we are at top level, we might need to invoke it manually or parse specific args.
|
|
55
|
+
// Easiest is to manually invoke program.parse with ['node', 'script', 'connection', 'add']
|
|
56
|
+
// BUT program.parse executes asynchronously usually? commander is synchronous by default but actions are async.
|
|
57
|
+
// Let's modify process.argv before parsing.
|
|
58
|
+
process.argv = [...process.argv.slice(0, 2), 'connection', 'add'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class ApiClient {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.config = config;
|
|
4
|
+
this.baseUrl = config.url.replace(/\/$/, ''); // Remove trailing slash
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async get(endpoint, params = {}, config = {}) {
|
|
8
|
+
throw new Error('Method not implemented');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async post(endpoint, data = {}, config = {}) {
|
|
12
|
+
throw new Error('Method not implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async put(endpoint, data = {}, config = {}) {
|
|
16
|
+
throw new Error('Method not implemented');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async delete(endpoint, config = {}) {
|
|
20
|
+
throw new Error('Method not implemented');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SaasClient } from './saas.js';
|
|
2
|
+
import { PaasClient } from './paas.js';
|
|
3
|
+
import { getActiveProfile } from '../config.js';
|
|
4
|
+
|
|
5
|
+
export async function createClient(configOrProfileName = null) {
|
|
6
|
+
let settings;
|
|
7
|
+
if (typeof configOrProfileName === 'object' && configOrProfileName !== null) {
|
|
8
|
+
settings = configOrProfileName;
|
|
9
|
+
} else if (typeof configOrProfileName === 'string') {
|
|
10
|
+
const { loadConfig } = await import('../config.js');
|
|
11
|
+
const config = await loadConfig();
|
|
12
|
+
settings = config.profiles[configOrProfileName];
|
|
13
|
+
} else {
|
|
14
|
+
settings = await getActiveProfile();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!settings) {
|
|
18
|
+
throw new Error("No active profile found. Please run 'mage-remote-run configure'.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (settings.type === 'ac-saas' || settings.type === 'saas') {
|
|
22
|
+
return new SaasClient(settings);
|
|
23
|
+
} else {
|
|
24
|
+
// magento-os, mage-os, ac-on-prem, ac-cloud-paas, paas
|
|
25
|
+
return new PaasClient(settings);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/api/paas.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import OAuth from 'oauth-1.0a';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { ApiClient } from './client.js';
|
|
5
|
+
import { OpenAPIClientAxios } from 'openapi-client-axios';
|
|
6
|
+
import { loadSpec } from './spec-loader.js';
|
|
7
|
+
|
|
8
|
+
export class PaasClient extends ApiClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
super(config);
|
|
11
|
+
this.openApi = null;
|
|
12
|
+
this.axiosInstance = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
if (this.openApi) return;
|
|
17
|
+
|
|
18
|
+
const definition = loadSpec(this.config.type);
|
|
19
|
+
this.openApi = new OpenAPIClientAxios({
|
|
20
|
+
definition,
|
|
21
|
+
axiosConfigDefaults: {
|
|
22
|
+
baseURL: `${this.baseUrl}/rest/V1`
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.axiosInstance = await this.openApi.getClient();
|
|
27
|
+
|
|
28
|
+
// Add auth interceptor
|
|
29
|
+
this.axiosInstance.interceptors.request.use((config) => {
|
|
30
|
+
// Handle Auth
|
|
31
|
+
if (this.config.auth.method === 'bearer') {
|
|
32
|
+
config.headers['Authorization'] = `Bearer ${this.config.auth.token}`;
|
|
33
|
+
} else if (this.config.auth.method === 'oauth1') {
|
|
34
|
+
// Calculate OAuth1 signature
|
|
35
|
+
// Note: config.url here might be relative or full depending on axios usage.
|
|
36
|
+
// axios instance url is typically combined.
|
|
37
|
+
|
|
38
|
+
// We need to construct the full URL for signing.
|
|
39
|
+
// config.baseURL + config.url
|
|
40
|
+
const baseURL = config.baseURL || '';
|
|
41
|
+
const url = baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, ''); // rudimentary join
|
|
42
|
+
|
|
43
|
+
const oauth = OAuth({
|
|
44
|
+
consumer: {
|
|
45
|
+
key: this.config.auth.consumerKey,
|
|
46
|
+
secret: this.config.auth.consumerSecret
|
|
47
|
+
},
|
|
48
|
+
signature_method: 'HMAC-SHA256',
|
|
49
|
+
hash_function(base_string, key) {
|
|
50
|
+
return crypto
|
|
51
|
+
.createHmac('sha256', key)
|
|
52
|
+
.update(base_string)
|
|
53
|
+
.digest('base64');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const requestData = {
|
|
58
|
+
url,
|
|
59
|
+
method: config.method.toUpperCase(),
|
|
60
|
+
data: config.params // OAuth 1.0a signs query params
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const token = {
|
|
64
|
+
key: this.config.auth.accessToken,
|
|
65
|
+
secret: this.config.auth.tokenSecret
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const authHeader = oauth.toHeader(oauth.authorize(requestData, token));
|
|
69
|
+
Object.assign(config.headers, authHeader);
|
|
70
|
+
}
|
|
71
|
+
return config;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async request(method, endpoint, data = {}, params = {}, config = {}) {
|
|
76
|
+
await this.init();
|
|
77
|
+
|
|
78
|
+
const cleanEndpoint = endpoint.replace(/^\//, '');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.axiosInstance({
|
|
82
|
+
method,
|
|
83
|
+
url: cleanEndpoint,
|
|
84
|
+
data,
|
|
85
|
+
params,
|
|
86
|
+
...config
|
|
87
|
+
});
|
|
88
|
+
return response.data;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.response) {
|
|
91
|
+
const newError = new Error(`API Error ${error.response.status}: ${JSON.stringify(error.response.data)}`);
|
|
92
|
+
newError.response = error.response;
|
|
93
|
+
throw newError;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async get(endpoint, params, config) { return this.request('get', endpoint, undefined, params, config); }
|
|
100
|
+
async post(endpoint, data, config) { return this.request('post', endpoint, data, undefined, config); }
|
|
101
|
+
async put(endpoint, data, config) { return this.request('put', endpoint, data, undefined, config); }
|
|
102
|
+
async delete(endpoint, config) { return this.request('delete', endpoint, undefined, undefined, config); }
|
|
103
|
+
}
|
package/lib/api/saas.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { ApiClient } from './client.js';
|
|
3
|
+
import { OpenAPIClientAxios } from 'openapi-client-axios';
|
|
4
|
+
import { loadSpec } from './spec-loader.js';
|
|
5
|
+
|
|
6
|
+
export class SaasClient extends ApiClient {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super(config);
|
|
9
|
+
this.token = null;
|
|
10
|
+
this.tokenExpiresAt = 0;
|
|
11
|
+
this.openApi = null;
|
|
12
|
+
this.axiosInstance = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
if (this.openApi) return;
|
|
17
|
+
|
|
18
|
+
const definition = loadSpec(this.config.type);
|
|
19
|
+
this.openApi = new OpenAPIClientAxios({
|
|
20
|
+
definition,
|
|
21
|
+
axiosConfigDefaults: {
|
|
22
|
+
baseURL: `${this.baseUrl}/rest/V1`, // Base for generated client
|
|
23
|
+
// Note: The definition might have servers block, but we override functionality.
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// We initialize the client but we will handle auth via interceptors or manual token injection
|
|
28
|
+
this.axiosInstance = await this.openApi.getClient();
|
|
29
|
+
|
|
30
|
+
// Add auth interceptor
|
|
31
|
+
this.axiosInstance.interceptors.request.use(async (config) => {
|
|
32
|
+
const token = await this.getToken();
|
|
33
|
+
config.headers['Authorization'] = `Bearer ${token}`;
|
|
34
|
+
return config;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getToken() {
|
|
39
|
+
if (this.token && Date.now() < this.tokenExpiresAt) {
|
|
40
|
+
return this.token;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tokenUrl = `${this.baseUrl}/oauth/token`;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Use raw axios for token fetch to avoid circular dependency or interceptor issues
|
|
47
|
+
const response = await axios.post(tokenUrl, {
|
|
48
|
+
grant_type: 'client_credentials',
|
|
49
|
+
client_id: this.config.auth.clientId,
|
|
50
|
+
client_secret: this.config.auth.clientSecret
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.token = response.data.access_token;
|
|
54
|
+
const expiresIn = response.data.expires_in || 3600;
|
|
55
|
+
this.tokenExpiresAt = Date.now() + (expiresIn * 1000) - 60000;
|
|
56
|
+
|
|
57
|
+
return this.token;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`Failed to obtain OAuth2 token: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async request(method, endpoint, data = {}, params = {}, config = {}) {
|
|
64
|
+
await this.init();
|
|
65
|
+
|
|
66
|
+
// Use the configured axios instance for generic requests
|
|
67
|
+
// Note: endpoint passed here is like 'store/websites'
|
|
68
|
+
// openapi-client-axios client base is usually set to server root or we adjusted it.
|
|
69
|
+
// If we set baseURL to /rest/V1, we just need the relative path.
|
|
70
|
+
|
|
71
|
+
// Ensure endpoint does not start with / if base has it, or handle cleanly
|
|
72
|
+
const cleanEndpoint = endpoint.replace(/^\//, '');
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.axiosInstance({
|
|
76
|
+
method,
|
|
77
|
+
url: cleanEndpoint,
|
|
78
|
+
data,
|
|
79
|
+
params,
|
|
80
|
+
...config
|
|
81
|
+
});
|
|
82
|
+
return response.data;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error.response) {
|
|
85
|
+
const newError = new Error(`API Error ${error.response.status}: ${JSON.stringify(error.response.data)}`);
|
|
86
|
+
newError.response = error.response;
|
|
87
|
+
throw newError;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async get(endpoint, params, config) { return this.request('get', endpoint, undefined, params, config); }
|
|
94
|
+
async post(endpoint, data, config) { return this.request('post', endpoint, data, undefined, config); }
|
|
95
|
+
async put(endpoint, data, config) { return this.request('put', endpoint, data, undefined, config); }
|
|
96
|
+
async delete(endpoint, config) { return this.request('delete', endpoint, undefined, undefined, config); }
|
|
97
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { OpenAPIClientAxios } from 'openapi-client-axios';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// Locate specs relative to project root (assuming we are in lib/api)
|
|
6
|
+
const SPEC_DIR = path.resolve(process.cwd(), 'api-specs/2.4.8');
|
|
7
|
+
|
|
8
|
+
export function loadSpec(type) {
|
|
9
|
+
let specFile;
|
|
10
|
+
if (type === 'ac-saas' || type === 'saas') {
|
|
11
|
+
specFile = 'swagger-saas.json';
|
|
12
|
+
} else {
|
|
13
|
+
// All others use PaaS/OpenSource spec
|
|
14
|
+
specFile = 'swagger-paas.json';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const filePath = path.join(SPEC_DIR, specFile);
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
throw new Error(`OpenAPI spec not found at: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
23
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, addProfile } from '../config.js';
|
|
2
|
+
import { printTable, handleError } from '../utils.js';
|
|
3
|
+
import { askForProfileSettings } from '../prompts.js';
|
|
4
|
+
import { createClient } from '../api/factory.js';
|
|
5
|
+
import { input, confirm, select } from '@inquirer/prompts';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export function registerConnectionCommands(program) {
|
|
10
|
+
const connections = program.command('connection').description('Manage connection profiles');
|
|
11
|
+
|
|
12
|
+
connections.command('add')
|
|
13
|
+
.description('Configure a new connection profile')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
console.log(chalk.blue('Configure a new connection Profile'));
|
|
16
|
+
try {
|
|
17
|
+
const name = await input({
|
|
18
|
+
message: 'Profile Name:',
|
|
19
|
+
validate: value => value ? true : 'Name is required'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const settings = await askForProfileSettings();
|
|
23
|
+
|
|
24
|
+
await addProfile(name, settings);
|
|
25
|
+
console.log(chalk.green(`\nProfile "${name}" saved successfully!`));
|
|
26
|
+
|
|
27
|
+
// Ask to set as active if multiple exist
|
|
28
|
+
const config = await loadConfig();
|
|
29
|
+
if (Object.keys(config.profiles).length > 1) {
|
|
30
|
+
const setActive = await confirm({
|
|
31
|
+
message: 'Set this as the active profile?',
|
|
32
|
+
default: true
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (setActive) {
|
|
36
|
+
config.activeProfile = name;
|
|
37
|
+
await saveConfig(config);
|
|
38
|
+
console.log(chalk.green(`Profile "${name}" set as active.`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e.name === 'ExitPromptError') {
|
|
43
|
+
console.log(chalk.yellow('\nConfiguration cancelled.'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
handleError(e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
connections.command('list')
|
|
51
|
+
.description('List connection profiles')
|
|
52
|
+
.action(async () => {
|
|
53
|
+
try {
|
|
54
|
+
const config = await loadConfig();
|
|
55
|
+
const rows = Object.entries(config.profiles || {}).map(([name, p]) => [
|
|
56
|
+
name,
|
|
57
|
+
p.type,
|
|
58
|
+
p.url,
|
|
59
|
+
name === config.activeProfile ? chalk.green('Yes') : 'No'
|
|
60
|
+
]);
|
|
61
|
+
printTable(['Name', 'Type', 'URL', 'Active'], rows);
|
|
62
|
+
} catch (e) { handleError(e); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
connections.command('search <query>')
|
|
66
|
+
.description('Search connection profiles')
|
|
67
|
+
.action(async (query) => {
|
|
68
|
+
try {
|
|
69
|
+
const config = await loadConfig();
|
|
70
|
+
const filtered = Object.entries(config.profiles || {}).filter(([name]) => name.includes(query));
|
|
71
|
+
const rows = filtered.map(([name, p]) => [name, p.type, p.url]);
|
|
72
|
+
printTable(['Name', 'Type', 'URL'], rows);
|
|
73
|
+
} catch (e) { handleError(e); }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
connections.command('delete <name>')
|
|
77
|
+
.description('Delete a connection profile')
|
|
78
|
+
.action(async (name) => {
|
|
79
|
+
try {
|
|
80
|
+
const config = await loadConfig();
|
|
81
|
+
if (!config.profiles[name]) throw new Error(`Profile ${name} not found`);
|
|
82
|
+
|
|
83
|
+
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Delete profile ${name}?` }]);
|
|
84
|
+
if (!confirm) return;
|
|
85
|
+
|
|
86
|
+
delete config.profiles[name];
|
|
87
|
+
if (config.activeProfile === name) config.activeProfile = null;
|
|
88
|
+
await saveConfig(config);
|
|
89
|
+
console.log(chalk.green(`Profile ${name} deleted.`));
|
|
90
|
+
} catch (e) { handleError(e); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
connections.command('edit <name>')
|
|
94
|
+
.description('Edit a connection profile')
|
|
95
|
+
.action(async (name) => {
|
|
96
|
+
try {
|
|
97
|
+
const config = await loadConfig();
|
|
98
|
+
const profile = config.profiles[name];
|
|
99
|
+
if (!profile) throw new Error(`Profile ${name} not found`);
|
|
100
|
+
|
|
101
|
+
const settings = await askForProfileSettings(profile);
|
|
102
|
+
|
|
103
|
+
config.profiles[name] = settings;
|
|
104
|
+
await saveConfig(config);
|
|
105
|
+
console.log(chalk.green(`Profile "${name}" updated.`));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (e.name === 'ExitPromptError') {
|
|
108
|
+
console.log(chalk.yellow('\nEdit cancelled.'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
handleError(e);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
connections.command('test')
|
|
116
|
+
.description('Test connection(s)')
|
|
117
|
+
.option('--all', 'Test all configured connections')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
try {
|
|
120
|
+
const config = await loadConfig();
|
|
121
|
+
|
|
122
|
+
if (options.all) {
|
|
123
|
+
const profiles = Object.keys(config.profiles || {});
|
|
124
|
+
if (profiles.length === 0) {
|
|
125
|
+
console.log(chalk.yellow('No profiles found.'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(chalk.blue(`Testing ${profiles.length} connections...\n`));
|
|
129
|
+
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const name of profiles) {
|
|
132
|
+
try {
|
|
133
|
+
const profileConfig = config.profiles[name];
|
|
134
|
+
const client = await createClient(profileConfig);
|
|
135
|
+
|
|
136
|
+
const start = Date.now();
|
|
137
|
+
await client.get('store/storeViews');
|
|
138
|
+
const duration = Date.now() - start;
|
|
139
|
+
|
|
140
|
+
results.push([name, chalk.green('SUCCESS'), `${duration}ms`]);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
results.push([name, chalk.red('FAILED'), e.message]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(chalk.bold('Connection Test Results:'));
|
|
147
|
+
printTable(['Profile', 'Status', 'Details'], results);
|
|
148
|
+
|
|
149
|
+
} else {
|
|
150
|
+
if (!config.activeProfile) {
|
|
151
|
+
console.log(chalk.yellow('No active profile configured. Run "connection add" or "connection test --all".'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(`Testing active connection: ${chalk.bold(config.activeProfile)}...`);
|
|
155
|
+
try {
|
|
156
|
+
const client = await createClient(); // Uses active profile
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
await client.get('store/storeViews');
|
|
159
|
+
const duration = Date.now() - start;
|
|
160
|
+
|
|
161
|
+
console.log(chalk.green(`\nā Connection successful! (${duration}ms)`));
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error(chalk.red('\nā Connection failed:'), e.message);
|
|
164
|
+
if (process.env.DEBUG) console.error(e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error(chalk.red('Test failed:'), e.message);
|
|
169
|
+
if (process.env.DEBUG) console.error(e);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
connections.command('status')
|
|
173
|
+
.description('Show current configuration status')
|
|
174
|
+
.action(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const config = await loadConfig();
|
|
177
|
+
if (!config.activeProfile) {
|
|
178
|
+
console.log(chalk.yellow('No active profile configured. Run "connection add" or "connection select".'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(chalk.bold('Active Profile:'), chalk.green(config.activeProfile));
|
|
182
|
+
const profile = config.profiles[config.activeProfile];
|
|
183
|
+
if (profile) {
|
|
184
|
+
console.log(`Type: ${profile.type}`);
|
|
185
|
+
console.log(`URL: ${profile.url}`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log(chalk.red('Profile not found in configuration!'));
|
|
188
|
+
}
|
|
189
|
+
} catch (e) { handleError(e); }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
connections.command('select')
|
|
193
|
+
.description('Select the active connection profile')
|
|
194
|
+
.action(async () => {
|
|
195
|
+
try {
|
|
196
|
+
const config = await loadConfig();
|
|
197
|
+
const profiles = Object.keys(config.profiles || {});
|
|
198
|
+
if (profiles.length === 0) {
|
|
199
|
+
console.log(chalk.yellow('No profiles found. Run "connection add" first.'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const selected = await select({
|
|
204
|
+
message: 'Select Active Profile:',
|
|
205
|
+
choices: profiles.map(p => ({ value: p, name: p })),
|
|
206
|
+
default: config.activeProfile
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
config.activeProfile = selected;
|
|
210
|
+
await saveConfig(config);
|
|
211
|
+
console.log(chalk.green(`Active profile set to "${selected}".`));
|
|
212
|
+
} catch (e) { handleError(e); }
|
|
213
|
+
});
|
|
214
|
+
}
|