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.
@@ -0,0 +1,196 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+
6
+ export function registerCustomersCommands(program) {
7
+ const customers = program.command('customer').description('Manage customers');
8
+
9
+ customers.command('list')
10
+ .option('-p, --page <number>', 'Page number', '1')
11
+ .option('-s, --size <number>', 'Page size', '20')
12
+ .action(async (options) => {
13
+ try {
14
+ const client = await createClient();
15
+ const params = {
16
+ 'searchCriteria[currentPage]': options.page,
17
+ 'searchCriteria[pageSize]': options.size
18
+ };
19
+ const data = await client.get('customers/search', params);
20
+ const rows = (data.items || []).map(c => [c.id, c.email, c.firstname, c.lastname, c.group_id]);
21
+ console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
22
+ printTable(['ID', 'Email', 'First Name', 'Last Name', 'Group'], rows);
23
+ } catch (e) { handleError(e); }
24
+ });
25
+
26
+ customers.command('search <query>')
27
+ .description('Search customers by email')
28
+ .action(async (query) => {
29
+ try {
30
+ const client = await createClient();
31
+ const params = {
32
+ 'searchCriteria[filter_groups][0][filters][0][field]': 'email',
33
+ 'searchCriteria[filter_groups][0][filters][0][value]': `%${query}%`,
34
+ 'searchCriteria[filter_groups][0][filters][0][condition_type]': 'like'
35
+ };
36
+ const data = await client.get('customers/search', params);
37
+ const rows = (data.items || []).map(c => [c.id, c.email, c.firstname, c.lastname]);
38
+ printTable(['ID', 'Email', 'Name', 'Lastname'], rows);
39
+ } catch (e) { handleError(e); }
40
+ });
41
+
42
+ customers.command('edit <id>')
43
+ .action(async (id) => {
44
+ try {
45
+ const client = await createClient();
46
+ const data = await client.get(`customers/${id}`);
47
+
48
+ const answers = await inquirer.prompt([
49
+ { name: 'firstname', message: 'First Name', default: data.firstname },
50
+ { name: 'lastname', message: 'Last Name', default: data.lastname },
51
+ { name: 'email', message: 'Email', default: data.email }
52
+ ]);
53
+
54
+ const payload = {
55
+ customer: {
56
+ ...data,
57
+ firstname: answers.firstname,
58
+ lastname: answers.lastname,
59
+ email: answers.email
60
+ }
61
+ };
62
+
63
+ await client.put(`customers/${id}`, payload);
64
+ console.log(chalk.green(`Customer ${id} updated.`));
65
+ } catch (e) { handleError(e); }
66
+ });
67
+
68
+ customers.command('show <customerId>')
69
+ .description('Show detailed customer information')
70
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
71
+ .action(async (customerId, options) => {
72
+ try {
73
+ const client = await createClient();
74
+ let headers = {};
75
+ if (options.format === 'json') headers['Accept'] = 'application/json';
76
+ else if (options.format === 'xml') headers['Accept'] = 'application/xml';
77
+
78
+ const data = await client.get(`customers/${customerId}`, {}, { headers });
79
+
80
+ if (options.format === 'json') {
81
+ console.log(JSON.stringify(data, null, 2));
82
+ return;
83
+ }
84
+ if (options.format === 'xml') {
85
+ console.log(data);
86
+ return;
87
+ }
88
+
89
+ console.log(chalk.bold.blue('\n👤 Customer Information'));
90
+ console.log(chalk.gray('━'.repeat(60)));
91
+
92
+ console.log(chalk.bold('\nℹ️ General Information'));
93
+ console.log(` ${chalk.bold('ID:')} ${data.id}`);
94
+ console.log(` ${chalk.bold('Email:')} ${data.email}`);
95
+ console.log(` ${chalk.bold('First Name:')} ${data.firstname}`);
96
+ console.log(` ${chalk.bold('Last Name:')} ${data.lastname}`);
97
+ console.log(` ${chalk.bold('Group ID:')} ${data.group_id}`);
98
+ console.log(` ${chalk.bold('Created At:')} ${data.created_at}`);
99
+ console.log(` ${chalk.bold('Updated At:')} ${data.updated_at}`);
100
+ console.log(` ${chalk.bold('Website ID:')} ${data.website_id}`);
101
+ console.log(` ${chalk.bold('Store ID:')} ${data.store_id}`);
102
+
103
+ if (data.addresses && data.addresses.length > 0) {
104
+ console.log(chalk.bold('\n📍 Addresses'));
105
+ data.addresses.forEach((addr, idx) => {
106
+ console.log(chalk.bold(` Address #${idx + 1} ${addr.default_billing ? '(Default Billing)' : ''} ${addr.default_shipping ? '(Default Shipping)' : ''}`));
107
+ const addressLines = [
108
+ ` ${addr.firstname} ${addr.lastname}`,
109
+ ...(addr.street || []).map(s => ` ${s}`),
110
+ ` ${addr.city}, ${addr.region ? addr.region.region : ''} ${addr.postcode}`,
111
+ ` ${addr.country_id}`,
112
+ ` T: ${addr.telephone}`
113
+ ];
114
+ console.log(addressLines.join('\n'));
115
+ console.log('');
116
+ });
117
+ }
118
+ console.log(chalk.gray('━'.repeat(60)));
119
+
120
+ } catch (e) { handleError(e); }
121
+ });
122
+
123
+ customers.command('delete <customerId>')
124
+ .description('Delete a customer')
125
+ .option('--force', 'Force delete without confirmation')
126
+ .action(async (customerId, options) => {
127
+ try {
128
+ if (!options.force) {
129
+ const { confirm } = await inquirer.prompt([{
130
+ type: 'confirm',
131
+ name: 'confirm',
132
+ message: `Are you sure you want to delete customer ${customerId}?`,
133
+ default: false
134
+ }]);
135
+ if (!confirm) {
136
+ console.log('Operation cancelled.');
137
+ return;
138
+ }
139
+ }
140
+
141
+ const client = await createClient();
142
+ await client.delete(`customers/${customerId}`);
143
+ console.log(chalk.green(`✅ Customer ${customerId} deleted.`));
144
+ } catch (e) { handleError(e); }
145
+ });
146
+
147
+ customers.command('confirm [customerId]')
148
+ .description('Resend customer confirmation email')
149
+ .option('--redirect-url <url>', 'Redirect URL after confirmation')
150
+ .action(async (customerId, options) => {
151
+ try {
152
+ const client = await createClient();
153
+ let email, websiteId;
154
+
155
+ if (customerId) {
156
+ try {
157
+ const customer = await client.get(`customers/${customerId}`);
158
+ email = customer.email;
159
+ websiteId = customer.website_id;
160
+ console.log(chalk.gray(`Fetched customer ${customerId}: ${email} (Website: ${websiteId})`));
161
+ } catch (e) {
162
+ throw new Error(`Customer ${customerId} not found.`);
163
+ }
164
+ } else {
165
+ const answers = await inquirer.prompt([
166
+ { name: 'email', message: 'Customer Email:' },
167
+ { name: 'websiteId', message: 'Website ID:', default: '1' }
168
+ ]);
169
+ email = answers.email;
170
+ websiteId = answers.websiteId;
171
+ }
172
+
173
+ if (!customerId && !options.redirectUrl) {
174
+ const { redirectUrl } = await inquirer.prompt([
175
+ { name: 'redirectUrl', message: 'Redirect URL (optional):' }
176
+ ]);
177
+ options.redirectUrl = redirectUrl;
178
+ }
179
+
180
+ const payload = {
181
+ email: email,
182
+ websiteId: parseInt(websiteId, 10),
183
+ redirectUrl: options.redirectUrl || undefined
184
+ };
185
+
186
+ await client.post('customers/confirm', payload);
187
+ console.log(chalk.green(`✅ Confirmation email sent to ${email}`));
188
+ } catch (e) {
189
+ if (e.response && e.response.status === 400 && e.response.data && e.response.data.message === "Confirmation isn't needed.") {
190
+ console.log(chalk.yellow('ℹ️ Confirmation is not needed for this customer (already confirmed).'));
191
+ } else {
192
+ handleError(e);
193
+ }
194
+ }
195
+ });
196
+ }
@@ -0,0 +1,49 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+
5
+ export function registerEavCommands(program) {
6
+ const eav = program.command('eav').description('Manage EAV attributes and sets');
7
+
8
+ // Attribute Sets
9
+ const attributeSets = eav.command('attribute-set').description('Manage attribute sets');
10
+
11
+ attributeSets.command('list')
12
+ .description('List all attribute sets')
13
+ .option('-p, --page <number>', 'Page number', '1')
14
+ .option('-s, --size <number>', 'Page size', '20')
15
+ .action(async (options) => {
16
+ try {
17
+ const client = await createClient();
18
+ const params = {
19
+ 'searchCriteria[currentPage]': options.page,
20
+ 'searchCriteria[pageSize]': options.size
21
+ };
22
+ const data = await client.get('eav/attribute-sets/list', params);
23
+ const rows = (data.items || []).map(set => [
24
+ set.attribute_set_id,
25
+ set.attribute_set_name,
26
+ set.entity_type_id,
27
+ set.sort_order
28
+ ]);
29
+ console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
30
+ printTable(['ID', 'Name', 'Entity Type ID', 'Sort Order'], rows);
31
+ } catch (e) { handleError(e); }
32
+ });
33
+
34
+ attributeSets.command('show <id>')
35
+ .description('Show attribute set details')
36
+ .action(async (id) => {
37
+ try {
38
+ const client = await createClient();
39
+ const data = await client.get(`eav/attribute-sets/${id}`);
40
+
41
+ console.log(chalk.bold.blue('\nAttribute Set Details:'));
42
+ console.log(`${chalk.bold('ID:')} ${data.attribute_set_id}`);
43
+ console.log(`${chalk.bold('Name:')} ${data.attribute_set_name}`);
44
+ console.log(`${chalk.bold('Entity Type ID:')} ${data.entity_type_id}`);
45
+ console.log(`${chalk.bold('Sort Order:')} ${data.sort_order}`);
46
+
47
+ } catch (e) { handleError(e); }
48
+ });
49
+ }
@@ -0,0 +1,247 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+
6
+ export function registerOrdersCommands(program) {
7
+ const orders = program.command('order').description('Manage orders');
8
+
9
+ orders.command('list')
10
+ .option('-p, --page <number>', 'Page number', '1')
11
+ .option('-s, --size <number>', 'Page size', '20')
12
+ .action(async (options) => {
13
+ try {
14
+ const client = await createClient();
15
+ const params = {
16
+ 'searchCriteria[currentPage]': options.page,
17
+ 'searchCriteria[pageSize]': options.size
18
+ };
19
+ const data = await client.get('orders', params);
20
+ const rows = (data.items || []).map(o => [o.entity_id, o.increment_id, o.status, o.grand_total, o.customer_email]);
21
+ console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
22
+ printTable(['ID', 'Increment ID', 'Status', 'Total', 'Email'], rows);
23
+ } catch (e) { handleError(e); }
24
+ });
25
+
26
+ orders.command('search <query>')
27
+ .description('Search orders by Increment ID')
28
+ .action(async (query) => {
29
+ try {
30
+ const client = await createClient();
31
+ const params = {
32
+ 'searchCriteria[filter_groups][0][filters][0][field]': 'increment_id',
33
+ 'searchCriteria[filter_groups][0][filters][0][value]': `%${query}%`,
34
+ 'searchCriteria[filter_groups][0][filters][0][condition_type]': 'like'
35
+ };
36
+ const data = await client.get('orders', params);
37
+ const rows = (data.items || []).map(o => [o.entity_id, o.increment_id, o.status, o.grand_total]);
38
+ printTable(['ID', 'Increment ID', 'Status', 'Total'], rows);
39
+ } catch (e) { handleError(e); }
40
+ });
41
+
42
+ orders.command('edit <id>')
43
+ .description('Update order status (via Comment)')
44
+ .action(async (id) => {
45
+ try {
46
+ const client = await createClient();
47
+ // Check if order exists?
48
+ // Just ask for status
49
+ const answers = await inquirer.prompt([
50
+ { name: 'status', message: 'New Status Code (e.g. pending, processing):' },
51
+ { name: 'comment', message: 'Comment:' }
52
+ ]);
53
+
54
+ // POST /V1/orders/:id/comments
55
+ await client.post(`orders/${id}/comments`, {
56
+ statusHistory: {
57
+ comment: answers.comment,
58
+ status: answers.status,
59
+ is_customer_notified: 0,
60
+ is_visible_on_front: 0
61
+ }
62
+ });
63
+ console.log(chalk.green(`Order ${id} updated.`));
64
+ } catch (e) { handleError(e); }
65
+ });
66
+ orders.command('show <identifier>')
67
+ .description('Show detailed order information by ID or Increment ID')
68
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
69
+ .action(async (identifier, options) => {
70
+ try {
71
+ await showOrder(identifier, options.format);
72
+ } catch (e) { handleError(e); }
73
+ });
74
+
75
+ orders.command('latest')
76
+ .description('List latest orders sorted by created_at DESC with selection')
77
+ .option('-p, --page <number>', 'Page number', '1')
78
+ .option('-z, --size <number>', 'Page size', '20')
79
+ .option('-s, --select', 'Interactive selection mode')
80
+ .action(async (options) => {
81
+ try {
82
+ const client = await createClient();
83
+ const params = {
84
+ 'searchCriteria[pageSize]': options.size,
85
+ 'searchCriteria[currentPage]': options.page,
86
+ 'searchCriteria[sortOrders][0][field]': 'created_at',
87
+ 'searchCriteria[sortOrders][0][direction]': 'DESC'
88
+ };
89
+ const data = await client.get('orders', params);
90
+ const items = data.items || [];
91
+
92
+ if (items.length === 0) {
93
+ console.log('No orders found.');
94
+ return;
95
+ }
96
+
97
+ const rows = (data.items || []).map(o => [o.entity_id, o.increment_id, o.status, o.grand_total, o.created_at]);
98
+ console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
99
+ printTable(['ID', 'Increment ID', 'Status', 'Total', 'Created At'], rows);
100
+
101
+ if (options.select) {
102
+ // Interactive selection
103
+ const choices = items.map(o => ({
104
+ name: `${o.increment_id} (ID: ${o.entity_id})`,
105
+ value: o.entity_id
106
+ }));
107
+
108
+ // Add exit option
109
+ choices.push({ name: 'Exit', value: null });
110
+
111
+ const { orderId } = await inquirer.prompt([{
112
+ type: 'list',
113
+ name: 'orderId',
114
+ message: `Select an order to view details (Page ${options.page}):`,
115
+ choices,
116
+ pageSize: 15
117
+ }]);
118
+
119
+ if (orderId) {
120
+ await showOrder(orderId, 'text');
121
+ }
122
+ }
123
+ } catch (e) { handleError(e); }
124
+ });
125
+ }
126
+
127
+ async function showOrder(identifier, format = 'text') {
128
+ const { createClient } = await import('../api/factory.js');
129
+ const { printTable } = await import('../utils.js');
130
+ const chalk = (await import('chalk')).default;
131
+ const client = await createClient();
132
+
133
+ let order;
134
+
135
+ // Map format to Accept header
136
+ let headers = {};
137
+ if (format === 'json') {
138
+ headers['Accept'] = 'application/json';
139
+ } else if (format === 'xml') {
140
+ headers['Accept'] = 'application/xml';
141
+ }
142
+
143
+ try {
144
+ order = await client.get(`orders/${identifier}`, {}, { headers });
145
+ } catch (e) {
146
+ // If 404, maybe it was an increment ID
147
+ // Or if user provided something that is definitely an increment ID
148
+ const params = {
149
+ 'searchCriteria[filter_groups][0][filters][0][field]': 'increment_id',
150
+ 'searchCriteria[filter_groups][0][filters][0][value]': identifier,
151
+ 'searchCriteria[filter_groups][0][filters][0][condition_type]': 'eq'
152
+ };
153
+ const searchData = await client.get('orders', params);
154
+ if (searchData.items && searchData.items.length > 0) {
155
+ order = searchData.items[0];
156
+ // If format is specific, we might want to re-fetch by ID with headers if search didn't return format?
157
+ // Search API usually returns JSON. If user wants XML, search return helps find ID, then we fetch XML.
158
+ if (format !== 'text') {
159
+ try {
160
+ order = await client.get(`orders/${order.entity_id}`, {}, { headers });
161
+ } catch (subError) {
162
+ // ignore or log? If we fail to get formatted, fallback or throw?
163
+ // If we found it via search, 'order' is the object.
164
+ // If format is json, we are good.
165
+ // If format is xml, we NEED to re-fetch because 'order' is currently a JS object from JSON response.
166
+ if (format === 'xml') throw subError;
167
+ }
168
+ }
169
+ } else {
170
+ throw new Error(`Order '${identifier}' not found.`);
171
+ }
172
+ }
173
+
174
+ if (format === 'xml') {
175
+ console.log(order); // Axios likely returns the XML string body
176
+ return;
177
+ }
178
+
179
+ if (format === 'json') {
180
+ // order is likely an object parsed by axios (if content-type was json)
181
+ console.log(JSON.stringify(order, null, 2));
182
+ return;
183
+ }
184
+
185
+ if (!order) throw new Error('Order not found');
186
+
187
+ const currency = order.order_currency_code || '';
188
+
189
+ console.log(chalk.bold.blue('\n📦 Order Information'));
190
+ console.log(chalk.gray('━'.repeat(50)));
191
+ console.log(`${chalk.bold('Increment ID:')} ${order.increment_id}`);
192
+ console.log(`${chalk.bold('Internal ID:')} ${order.entity_id}`);
193
+ console.log(`${chalk.bold('Status:')} ${statusColor(order.status)} (${order.state})`);
194
+ console.log(`${chalk.bold('Created At:')} ${order.created_at}`);
195
+ console.log(`${chalk.bold('Customer:')} ${order.customer_firstname} ${order.customer_lastname} <${order.customer_email}>`);
196
+ console.log(`${chalk.bold('Grand Total:')} ${chalk.green(order.grand_total + ' ' + currency)}`);
197
+ console.log(chalk.gray('━'.repeat(50)));
198
+
199
+ if (order.items && order.items.length > 0) {
200
+ console.log(chalk.bold('\n🛒 Items'));
201
+ const itemRows = order.items.map(item => [
202
+ item.sku,
203
+ item.name,
204
+ Math.floor(item.qty_ordered),
205
+ `${item.price} ${currency}`,
206
+ `${item.row_total} ${currency}`
207
+ ]);
208
+ printTable(['SKU', 'Name', 'Qty', 'Price', 'Subtotal'], itemRows);
209
+ }
210
+
211
+ if (order.billing_address || order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address) {
212
+ console.log(chalk.bold('\n📍 Addresses'));
213
+
214
+ if (order.billing_address) {
215
+ console.log(chalk.bold.underline('Billing Address:'));
216
+ printAddress(order.billing_address);
217
+ }
218
+
219
+ const shippingAddress = order.extension_attributes?.shipping_assignments?.[0]?.shipping?.address;
220
+ if (shippingAddress) {
221
+ console.log(chalk.bold.underline('\nShipping Address:'));
222
+ printAddress(shippingAddress);
223
+ }
224
+ }
225
+ }
226
+
227
+ function statusColor(status) {
228
+ // Basic color coding
229
+ if (['pending'].includes(status)) return status;
230
+ if (['processing'].includes(status)) return status;
231
+ if (['complete'].includes(status)) return status; // green?
232
+ // Since we don't have direct chalk access in this helper easily without passing it,
233
+ // and I don't want to import it again if I can avoid it or I can just return string.
234
+ // Actually, I can import properly in module scope if I change top imports but I am overwriting the file so I can add imports.
235
+ return status;
236
+ }
237
+
238
+ function printAddress(addr) {
239
+ const lines = [
240
+ `${addr.firstname} ${addr.lastname}`,
241
+ ...(addr.street || []),
242
+ `${addr.city}, ${addr.region || ''} ${addr.postcode}`,
243
+ addr.country_id,
244
+ addr.telephone ? `T: ${addr.telephone}` : null
245
+ ].filter(Boolean);
246
+ console.log(lines.join('\n'));
247
+ }
@@ -0,0 +1,133 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+
5
+ export function registerProductsCommands(program) {
6
+ const products = program.command('product').description('Manage products');
7
+
8
+ products.command('types')
9
+ .description('List available product types')
10
+ .action(async () => {
11
+ try {
12
+ const client = await createClient();
13
+ const data = await client.get('products/types');
14
+ const rows = (data || []).map(t => [t.name, t.label]);
15
+ printTable(['Name (ID)', 'Label'], rows);
16
+ } catch (e) { handleError(e); }
17
+ });
18
+
19
+ const attributes = products.command('attribute').description('Manage product attributes');
20
+
21
+ attributes.command('list')
22
+ .description('List product attributes')
23
+ .option('-p, --page <number>', 'Page number', '1')
24
+ .option('-s, --size <number>', 'Page size', '20')
25
+ .action(async (options) => {
26
+ try {
27
+ const client = await createClient();
28
+ const params = {
29
+ 'searchCriteria[currentPage]': options.page,
30
+ 'searchCriteria[pageSize]': options.size,
31
+ 'searchCriteria[sortOrders][0][field]': 'attribute_code',
32
+ 'searchCriteria[sortOrders][0][direction]': 'ASC'
33
+ };
34
+ const data = await client.get('products/attributes', params);
35
+ const rows = (data.items || []).map(a => [
36
+ a.attribute_id,
37
+ a.attribute_code,
38
+ a.default_frontend_label,
39
+ a.is_required,
40
+ a.is_user_defined
41
+ ]);
42
+ console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
43
+ printTable(['ID', 'Code', 'Label', 'Required', 'User Defined'], rows);
44
+ } catch (e) { handleError(e); }
45
+ });
46
+
47
+ attributes.command('show <attributeCode>')
48
+ .description('Show product attribute details')
49
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
50
+ .action(async (attributeCode, options) => {
51
+ try {
52
+ const client = await createClient();
53
+ let headers = {};
54
+ if (options.format === 'json') headers['Accept'] = 'application/json';
55
+ else if (options.format === 'xml') headers['Accept'] = 'application/xml';
56
+
57
+ let data;
58
+ try {
59
+ data = await client.get(`products/attributes/${attributeCode}`, {}, { headers });
60
+ } catch (e) {
61
+ throw new Error(`Attribute '${attributeCode}' not found.`);
62
+ }
63
+
64
+ if (options.format === 'json') {
65
+ console.log(JSON.stringify(data, null, 2));
66
+ return;
67
+ }
68
+ if (options.format === 'xml') {
69
+ console.log(data);
70
+ return;
71
+ }
72
+
73
+ console.log(chalk.bold.blue('\n📦 Attribute Information'));
74
+ console.log(chalk.gray('━'.repeat(60)));
75
+
76
+ // Helper for boolean display
77
+ const showBool = (val) => {
78
+ const isTrue = val === '1' || val === 1 || val === true || val === 'true';
79
+ return isTrue ? chalk.green('✅ Yes') : chalk.red('❌ No');
80
+ };
81
+
82
+ // General
83
+ console.log(chalk.bold('\nℹ️ General Information'));
84
+ console.log(` ${chalk.bold('ID:')} ${data.attribute_id}`);
85
+ console.log(` ${chalk.bold('Code:')} ${data.attribute_code}`);
86
+ console.log(` ${chalk.bold('Default Label:')} ${data.default_frontend_label}`);
87
+ console.log(` ${chalk.bold('Required:')} ${showBool(data.is_required)}`);
88
+ console.log(` ${chalk.bold('User Defined:')} ${showBool(data.is_user_defined)}`);
89
+ console.log(` ${chalk.bold('Unique:')} ${showBool(data.is_unique)}`);
90
+
91
+ // Frontend Properties
92
+ console.log(chalk.bold('\n🎨 Frontend Properties'));
93
+ console.log(` ${chalk.bold('Input Type:')} ${data.frontend_input}`);
94
+ console.log(` ${chalk.bold('Class:')} ${data.frontend_class || '-'}`);
95
+ console.log(` ${chalk.bold('Visible on Front:')} ${showBool(data.is_visible_on_front)}`);
96
+ console.log(` ${chalk.bold('HTML Allowed:')} ${showBool(data.is_html_allowed_on_front)}`);
97
+
98
+ // Storefront Properties
99
+ console.log(chalk.bold('\n🏪 Storefront Properties'));
100
+ console.log(` ${chalk.bold('Searchable:')} ${showBool(data.is_searchable)}`);
101
+ console.log(` ${chalk.bold('Filterable:')} ${showBool(data.is_filterable_in_search)}`);
102
+ console.log(` ${chalk.bold('Comparable:')} ${showBool(data.is_comparable)}`);
103
+ console.log(` ${chalk.bold('Sort By:')} ${showBool(data.used_for_sort_by)}`);
104
+ console.log(` ${chalk.bold('Promo Rules:')} ${showBool(data.is_used_for_promo_rules)}`);
105
+
106
+ console.log(chalk.gray('━'.repeat(60)));
107
+
108
+ if (data.options && data.options.length > 0) {
109
+ // Filter out empty options if needed, but showing all is safer
110
+ // Often option value '' is a placeholder
111
+ const visibleOptions = data.options.filter(o => o.value !== '' && o.label !== '');
112
+ if (visibleOptions.length > 0) {
113
+ console.log(chalk.bold('\n📋 Options'));
114
+ const optRows = visibleOptions.map(o => [o.value, o.label]);
115
+ printTable(['Value', 'Label'], optRows);
116
+ }
117
+ }
118
+
119
+ } catch (e) { handleError(e); }
120
+ });
121
+
122
+ const attributeTypes = attributes.command('type').description('Manage attribute types');
123
+ attributeTypes.command('list')
124
+ .description('List product attribute types')
125
+ .action(async () => {
126
+ try {
127
+ const client = await createClient();
128
+ const data = await client.get('products/attributes/types');
129
+ const rows = (data || []).map(t => [t.value, t.label]);
130
+ printTable(['Value', 'Label'], rows);
131
+ } catch (e) { handleError(e); }
132
+ });
133
+ }