@deerdesign/deertools 1.0.3 → 1.2.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/src/doctor.js ADDED
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const net = require('net');
6
+ const credentials = require('./credentials');
7
+ const { ALL_PRODUCTS } = require('./registry');
8
+ const { fetchText } = require('./http');
9
+ const { c } = require('./formatters');
10
+ const { loadSchemas } = require('./baselinker/crawler');
11
+ const { handleMcpMessage } = require('./baselinker/mcp-server');
12
+ const { getClient } = require('./baselinker/client');
13
+
14
+ function checkLoopbackPort(port) {
15
+ return new Promise(resolve => {
16
+ const srv = net.createServer();
17
+ srv.once('error', () => resolve(false));
18
+ srv.once('listening', () => {
19
+ srv.close(() => resolve(true));
20
+ });
21
+ srv.listen(port, '127.0.0.1');
22
+ });
23
+ }
24
+
25
+ async function cmdDoctor(pos, flags) {
26
+ const isJson = flags.json;
27
+ const out = (s = '') => { if (!isJson) process.stdout.write(`${s}\n`); };
28
+
29
+ out(`\n${c.bold}=== Diagnostyka środowiska DeerTools CLI (Doctor) ===${c.reset}\n`);
30
+
31
+ const report = {
32
+ nodeVersion: process.version,
33
+ os: `${os.type()} ${os.release()} (${os.arch()})`,
34
+ credentialsFile: { path: credentials.credentialsFile(), exists: false, permissionsOk: false },
35
+ ports: {},
36
+ endpoints: {},
37
+ credentials: {},
38
+ baselinkerMcp: {},
39
+ mcpClients: {},
40
+ summary: { passed: 0, warnings: 0, errors: 0 },
41
+ };
42
+
43
+ // 1. Node version check
44
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
45
+ if (nodeMajor >= 18) {
46
+ out(` ${c.green}✓${c.reset} Node.js: ${process.version} (wymagane >= 18)`);
47
+ report.summary.passed++;
48
+ } else {
49
+ out(` ${c.red}✗${c.reset} Node.js: ${process.version} (zbyt stara wersja, zaktualizuj do >= 18)`);
50
+ report.summary.errors++;
51
+ }
52
+
53
+ // 2. Credentials file permissions check
54
+ const credPath = credentials.credentialsFile();
55
+ report.credentialsFile.path = credPath;
56
+ if (fs.existsSync(credPath)) {
57
+ report.credentialsFile.exists = true;
58
+ try {
59
+ const stat = fs.statSync(credPath);
60
+ if (process.platform === 'win32' || (stat.mode & 0o077) === 0) {
61
+ out(` ${c.green}✓${c.reset} Plik poświadczeń: ${credPath} (uprawnienia 0600 bezpieczne)`);
62
+ report.credentialsFile.permissionsOk = true;
63
+ report.summary.passed++;
64
+ } else {
65
+ out(` ${c.yellow}⚠${c.reset} Plik poświadczeń: ${credPath} (zbyt otwarte uprawnienia: ${(stat.mode & 0o777).toString(8)}, zalecane 0600)`);
66
+ report.summary.warnings++;
67
+ }
68
+ } catch (_) {
69
+ out(` ${c.green}✓${c.reset} Plik poświadczeń: ${credPath}`);
70
+ report.summary.passed++;
71
+ }
72
+ } else {
73
+ out(` ${c.dim}–${c.reset} Plik poświadczeń: jeszcze nie utworzony (zaloguj się przez deertools login)`);
74
+ }
75
+
76
+ // 3. Loopback ports check
77
+ out(`\n${c.bold}Dostępność portów lokalnych:${c.reset}`);
78
+ const portsToCheck = [
79
+ { port: 3000, desc: 'OAuth loopback logowania z przeglądarki' },
80
+ { port: 3456, desc: 'BaseLinker Web Hub & MCP SSE Server' }
81
+ ];
82
+ for (const { port, desc } of portsToCheck) {
83
+ const portFree = await checkLoopbackPort(port);
84
+ report.ports[port] = portFree;
85
+ if (portFree) {
86
+ out(` ${c.green}✓${c.reset} Port lokalny ${port}: wolny (${desc})`);
87
+ report.summary.passed++;
88
+ } else {
89
+ out(` ${c.yellow}⚠${c.reset} Port lokalny ${port}: zajęty przez inny proces (CLI użyje portu alternatywnego)`);
90
+ report.summary.warnings++;
91
+ }
92
+ }
93
+
94
+ // 4. Products & BaseLinker connectivity check
95
+ out(`\n${c.bold}Łączność sieciowa z serwisami produkcyjnymi:${c.reset}`);
96
+ for (const product of ALL_PRODUCTS) {
97
+ const t0 = Date.now();
98
+ try {
99
+ let res = await fetchText(`${product.site}/llms.txt`, { timeoutMs: 3000 });
100
+ if (!res.ok && product.id === 'skills') {
101
+ res = await fetchText('https://id-preview--162a014c-053f-4837-9eaf-fae7b3e01417.lovable.app', { timeoutMs: 3000 });
102
+ }
103
+ const dt = Date.now() - t0;
104
+ if (res.ok) {
105
+ out(` ${c.green}✓${c.reset} ${product.name.padEnd(25)} [${dt}ms] HTTP ${res.status}`);
106
+ report.endpoints[product.id] = { status: res.status, pingMs: dt, ok: true };
107
+ report.summary.passed++;
108
+ } else {
109
+ out(` ${c.yellow}⚠${c.reset} ${product.name.padEnd(25)} [${dt}ms] HTTP ${res.status}`);
110
+ report.endpoints[product.id] = { status: res.status, pingMs: dt, ok: false };
111
+ report.summary.warnings++;
112
+ }
113
+ } catch (e) {
114
+ if (product.id === 'skills') {
115
+ try {
116
+ const res = await fetchText('https://ezspmlsnmxuhtzvwrnoj.supabase.co/functions/v1/skills-api', { timeoutMs: 3000 });
117
+ const dt = Date.now() - t0;
118
+ if (res.status > 0) {
119
+ out(` ${c.green}✓${c.reset} ${product.name.padEnd(25)} [${dt}ms] HTTP ${res.status} (cloud hub)`);
120
+ report.endpoints[product.id] = { status: res.status, pingMs: dt, ok: true };
121
+ report.summary.passed++;
122
+ continue;
123
+ }
124
+ } catch (_) {}
125
+ }
126
+ const dt = Date.now() - t0;
127
+ out(` ${c.red}✗${c.reset} ${product.name.padEnd(25)} [${dt}ms] ${e.message}`);
128
+ report.endpoints[product.id] = { status: 0, pingMs: dt, ok: false, error: e.message };
129
+ report.summary.errors++;
130
+ }
131
+ }
132
+
133
+ // BaseLinker API ping
134
+ const tBL0 = Date.now();
135
+ try {
136
+ const resBL = await fetchText('https://api.baselinker.com/', { timeoutMs: 3000 });
137
+ const dtBL = Date.now() - tBL0;
138
+ if (resBL.ok) {
139
+ out(` ${c.green}✓${c.reset} ${'BaseLinker API'.padEnd(25)} [${dtBL}ms] HTTP ${resBL.status}`);
140
+ report.endpoints.baselinker = { status: resBL.status, pingMs: dtBL, ok: true };
141
+ report.summary.passed++;
142
+ } else {
143
+ out(` ${c.yellow}⚠${c.reset} ${'BaseLinker API'.padEnd(25)} [${dtBL}ms] HTTP ${resBL.status}`);
144
+ report.endpoints.baselinker = { status: resBL.status, pingMs: dtBL, ok: false };
145
+ report.summary.warnings++;
146
+ }
147
+ } catch (e) {
148
+ const dtBL = Date.now() - tBL0;
149
+ out(` ${c.red}✗${c.reset} ${'BaseLinker API'.padEnd(25)} [${dtBL}ms] ${e.message}`);
150
+ report.endpoints.baselinker = { status: 0, pingMs: dtBL, ok: false, error: e.message };
151
+ report.summary.errors++;
152
+ }
153
+
154
+ // 5. Auth / Credentials status check
155
+ out(`\n${c.bold}Status kluczy i sesji logowania:${c.reset}`);
156
+ let loggedInCount = 0;
157
+ for (const product of ALL_PRODUCTS) {
158
+ const found = credentials.getKey(product);
159
+ if (!found) {
160
+ out(` ${c.dim}–${c.reset} ${product.name.padEnd(25)} brak logowania (zaloguj: deertools login ${product.id})`);
161
+ report.credentials[product.id] = { loggedIn: false };
162
+ continue;
163
+ }
164
+ const exp = credentials.expiryInfo(found.entry);
165
+ if (exp.expired) {
166
+ out(` ${c.red}✗${c.reset} ${product.name.padEnd(25)} klucz WYGASŁ! (odnów: deertools login ${product.id})`);
167
+ report.credentials[product.id] = { loggedIn: false, expired: true };
168
+ report.summary.errors++;
169
+ } else {
170
+ const daysStr = exp.daysLeft != null ? `ważny jeszcze ${exp.daysLeft} dni` : 'bez limitu';
171
+ const warn = exp.daysLeft != null && exp.daysLeft < 7;
172
+ if (warn) {
173
+ out(` ${c.yellow}⚠${c.reset} ${product.name.padEnd(25)} zalogowany (${daysStr} - wkrótce wygaśnie)`);
174
+ report.summary.warnings++;
175
+ } else {
176
+ out(` ${c.green}✓${c.reset} ${product.name.padEnd(25)} zalogowany (${daysStr})`);
177
+ report.summary.passed++;
178
+ }
179
+ loggedInCount++;
180
+ report.credentials[product.id] = { loggedIn: true, daysLeft: exp.daysLeft, email: found.entry?.email };
181
+ }
182
+ }
183
+
184
+ // BaseLinker Hub token check
185
+ const blToken = credentials.getBaseLinkerToken();
186
+ if (blToken) {
187
+ try {
188
+ const client = getClient(blToken);
189
+ const test = await client.testConnection();
190
+ const rate = client.getRateLimitStatus();
191
+ if (test.ok) {
192
+ out(` ${c.green}✓${c.reset} ${'BaseLinker Hub'.padEnd(25)} zalogowany (${credentials.mask(blToken)} · ${test.inventories.length} katalog(ów) · limit ${rate.used}/${rate.max} req/min)`);
193
+ report.credentials.baselinker = { loggedIn: true, inventories: test.inventories.length, rateLimit: rate };
194
+ report.summary.passed++;
195
+ } else {
196
+ out(` ${c.yellow}⚠${c.reset} ${'BaseLinker Hub'.padEnd(25)} token zapisany, ale API zgłasza: ${test.error}`);
197
+ report.credentials.baselinker = { loggedIn: false, error: test.error };
198
+ report.summary.warnings++;
199
+ }
200
+ } catch (err) {
201
+ out(` ${c.yellow}⚠${c.reset} ${'BaseLinker Hub'.padEnd(25)} błąd weryfikacji: ${err.message}`);
202
+ report.credentials.baselinker = { loggedIn: false, error: err.message };
203
+ report.summary.warnings++;
204
+ }
205
+ } else {
206
+ out(` ${c.dim}–${c.reset} ${'BaseLinker Hub'.padEnd(25)} brak logowania (zaloguj: deertools login baselinker)`);
207
+ report.credentials.baselinker = { loggedIn: false };
208
+ }
209
+
210
+ // 6. Lokalny silnik BaseLinker MCP (Handshake & Schematy)
211
+ out(`\n${c.bold}Lokalny silnik BaseLinker MCP (Handshake & Schematy):${c.reset}`);
212
+ report.baselinkerMcp = { schemasOk: false, mcpHandshakeOk: false, toolsCount: 0 };
213
+
214
+ try {
215
+ const schemas = loadSchemas();
216
+ const schemaCount = Object.keys(schemas).length;
217
+ if (schemaCount >= 200) {
218
+ const catCount = Object.values(schemas).filter(s => s.category === 'Product catalog').length;
219
+ out(` ${c.green}✓${c.reset} Baza schematów metod: załadowano ${schemaCount} metod (w tym ${catCount} katalogowych)`);
220
+ report.baselinkerMcp.schemasOk = true;
221
+ report.baselinkerMcp.methodsCount = schemaCount;
222
+ report.summary.passed++;
223
+ } else {
224
+ out(` ${c.yellow}⚠${c.reset} Baza schematów metod: załadowano tylko ${schemaCount} metod (odśwież: deertools baselinker sync-schema)`);
225
+ report.summary.warnings++;
226
+ }
227
+ } catch (err) {
228
+ out(` ${c.red}✗${c.reset} Baza schematów metod: błąd ładowania: ${err.message}`);
229
+ report.summary.errors++;
230
+ }
231
+
232
+ try {
233
+ const initRes = await handleMcpMessage({ id: 'test-init', method: 'initialize', params: {} });
234
+ const toolsRes = await handleMcpMessage({ id: 'test-tools', method: 'tools/list', params: {} });
235
+ const isProtoOk = initRes && initRes.result && initRes.result.protocolVersion === '2024-11-05';
236
+ const toolsCount = toolsRes?.result?.tools?.length || 0;
237
+
238
+ if (isProtoOk && toolsCount >= 15) {
239
+ out(` ${c.green}✓${c.reset} Silnik MCP (JSON-RPC): protokół 2024-11-05, gotowych ${toolsCount} dedykowanych narzędzi katalogu`);
240
+ report.baselinkerMcp.mcpHandshakeOk = true;
241
+ report.baselinkerMcp.toolsCount = toolsCount;
242
+ report.summary.passed++;
243
+ } else {
244
+ out(` ${c.red}✗${c.reset} Silnik MCP (JSON-RPC): niepoprawna odpowiedź inicjalizacyjna`);
245
+ report.summary.errors++;
246
+ }
247
+ } catch (err) {
248
+ out(` ${c.red}✗${c.reset} Silnik MCP (JSON-RPC): błąd wykonania: ${err.message}`);
249
+ report.summary.errors++;
250
+ }
251
+
252
+ // 7. MCP Clients check
253
+ out(`\n${c.bold}Wykryte konfiguracje MCP dla agentów AI:${c.reset}`);
254
+ const mcpLocations = [
255
+ { name: 'Cursor IDE (global)', path: path.join(os.homedir(), '.cursor', 'mcp.json') },
256
+ { name: 'Cursor Workspace', path: path.join(process.cwd(), '.cursor', 'mcp.json') },
257
+ { name: 'Antigravity / Gemini Workspace', path: path.join(process.cwd(), '.agents', 'mcp_config.json') },
258
+ ];
259
+ for (const loc of mcpLocations) {
260
+ if (fs.existsSync(loc.path)) {
261
+ try {
262
+ const json = JSON.parse(fs.readFileSync(loc.path, 'utf8'));
263
+ const srvs = Object.keys(json.mcpServers || {}).filter(k => k.startsWith('deer-'));
264
+ const hasBL = srvs.includes('deer-baselinker');
265
+ out(` ${c.green}✓${c.reset} ${loc.name}: skonfigurowano ${srvs.length} serwerów${hasBL ? ' (w tym deer-baselinker)' : ''}`);
266
+ report.mcpClients[loc.name] = { configured: true, count: srvs.length, servers: srvs, hasBaseLinker: hasBL };
267
+ report.summary.passed++;
268
+ } catch (_) {
269
+ out(` ${c.yellow}⚠${c.reset} ${loc.name}: plik istnieje, ale ma niepoprawny format JSON`);
270
+ report.summary.warnings++;
271
+ }
272
+ } else {
273
+ out(` ${c.dim}–${c.reset} ${loc.name}: brak (zainstaluj: deertools mcp install)`);
274
+ report.mcpClients[loc.name] = { configured: false };
275
+ }
276
+ }
277
+
278
+ out(`\n${c.bold}Podsumowanie diagnostyki:${c.reset} ${c.green}${report.summary.passed} OK${c.reset}, ${c.yellow}${report.summary.warnings} ostrzeżeń${c.reset}, ${c.red}${report.summary.errors} błędów${c.reset}\n`);
279
+
280
+ if (isJson) {
281
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
282
+ }
283
+ return report;
284
+ }
285
+
286
+ module.exports = { cmdDoctor };
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { getProduct } = require('./registry');
5
+ const { apiRequest, explain } = require('./http');
6
+ const { c, formatTable } = require('./formatters');
7
+ const { logAudit } = require('./audit');
8
+
9
+ function out(s = '') { process.stdout.write(`${s}\n`); }
10
+ function err(s = '') { process.stderr.write(`${s}\n`); }
11
+ function fail(msg, code = 1) {
12
+ const e = new Error(msg);
13
+ e.exitCode = code;
14
+ throw e;
15
+ }
16
+
17
+ // ==================== KATALOG ====================
18
+ async function cmdCatalog(pos, flags) {
19
+ const action = pos[0] || 'help';
20
+ const product = getProduct('katalog');
21
+
22
+ if (action === 'search') {
23
+ const query = pos.slice(1).join(' ') || flags.q || flags.search || '';
24
+ if (!query) fail('Użycie: deertools catalog search <fraza> [--package smart-life|modern-life|high-life] [--limit 20]', 2);
25
+
26
+ const q = { search: query };
27
+ if (flags.package || flags.program) q.program = flags.package || flags.program;
28
+ if (flags.limit) q.limit = flags.limit;
29
+ if (flags.category) q.category = flags.category;
30
+
31
+ const t0 = Date.now();
32
+ const res = await apiRequest(product, { method: 'GET', path: '/products', query: q });
33
+ logAudit({ product: 'katalog', method: 'GET', path: '/products', status: res.status, duration_ms: Date.now() - t0 });
34
+
35
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
36
+ const data = res.json || {};
37
+ const items = data.data || [];
38
+
39
+ if (flags.json) { out(JSON.stringify(data, null, 2)); return; }
40
+ if (flags.quiet || flags.q) { for (const it of items) out(it.sku || it.id); return; }
41
+
42
+ out(`\n${c.bold}Wyniki wyszukiwania w Katalogu (${items.length} z ${data.count || items.length}):${c.reset}`);
43
+ const headers = ['SKU', 'Nazwa produktu', 'Kategoria', 'Cena brutto', 'Pakiety'];
44
+ const rows = items.map(it => [
45
+ it.sku || '',
46
+ it.name || '',
47
+ it.category_name || (it.category && it.category.name) || '',
48
+ it.price != null ? `${it.price.toFixed(2)} zł` : '(ukryta)',
49
+ (it.packages || []).join(', ') || ((it.tags || []).filter(t => ['SMART', 'MODERN', 'HIGH'].includes(t)).join(', ')),
50
+ ]);
51
+ out(formatTable(headers, rows));
52
+ return;
53
+ }
54
+
55
+ if (action === 'get') {
56
+ const idOrSku = pos[1];
57
+ if (!idOrSku) fail('Użycie: deertools catalog get <id_lub_sku>', 2);
58
+ const res = await apiRequest(product, { method: 'GET', path: `/products/${encodeURIComponent(idOrSku)}` });
59
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
60
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
61
+ const p = res.json || {};
62
+ out(`\n${c.bold}${p.name || 'Produkt'}${c.reset} (SKU: ${p.sku})`);
63
+ out(` Cena: ${p.price != null ? `${p.price} zł` : '(brak)'} · Kategoria: ${p.category?.name || p.category_id || '–'}`);
64
+ out(` Producent: ${p.manufacturer?.name || p.manufacturer_id || '–'} · EAN: ${p.ean || '–'}`);
65
+ out(` Zdjęcia (${(p.images || []).length}): ${(p.images || []).slice(0, 3).join(', ')}`);
66
+ return;
67
+ }
68
+
69
+ if (action === 'categories') {
70
+ const res = await apiRequest(product, { method: 'GET', path: '/categories' });
71
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
72
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
73
+ const cats = res.json?.data || res.json || [];
74
+ out(`\n${c.bold}Kategorie wykończeniowe Deer Design (${cats.length}):${c.reset}`);
75
+ for (const cat of cats.slice(0, 30)) {
76
+ out(` • ${String(cat.name).padEnd(30)} ${c.dim}(slug: ${cat.slug || cat.id})${c.reset}`);
77
+ }
78
+ return;
79
+ }
80
+
81
+ if (action === 'manufacturers') {
82
+ const res = await apiRequest(product, { method: 'GET', path: '/manufacturers' });
83
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
84
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
85
+ const mfs = res.json?.data || res.json || [];
86
+ out(`\n${c.bold}Producenci w Katalogu Connect (${mfs.length}):${c.reset}`);
87
+ for (const m of mfs.slice(0, 35)) {
88
+ out(` • ${String(m.name).padEnd(28)} ${m.country ? `[${m.country}]` : ''}`);
89
+ }
90
+ return;
91
+ }
92
+
93
+ out(`\n${c.bold}Polecenia Katalogu Connect:${c.reset}
94
+ deertools catalog search <fraza> [--package modern-life] [--json]
95
+ deertools catalog get <id_lub_sku> [--json]
96
+ deertools catalog categories [--json]
97
+ deertools catalog manufacturers [--json]
98
+ `);
99
+ }
100
+
101
+ // ==================== DOSTAWY ====================
102
+ async function cmdDeliveries(pos, flags) {
103
+ const action = pos[0] || 'list';
104
+ const product = getProduct('dostawy');
105
+
106
+ if (action === 'list') {
107
+ const q = {};
108
+ if (flags.status) q.status = flags.status;
109
+ if (flags.limit) q.limit = flags.limit;
110
+ if (flags.warehouse) q.warehouse_id = flags.warehouse;
111
+
112
+ const t0 = Date.now();
113
+ const res = await apiRequest(product, { method: 'GET', path: '/deliveries-api/deliveries', query: q });
114
+ logAudit({ product: 'dostawy', method: 'GET', path: '/deliveries-api/deliveries', status: res.status, duration_ms: Date.now() - t0 });
115
+
116
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
117
+ const data = res.json || [];
118
+ const list = Array.isArray(data) ? data : (data.data || []);
119
+
120
+ if (flags.json) { out(JSON.stringify(data, null, 2)); return; }
121
+ if (flags.quiet || flags.q) { for (const d of list) out(d.id); return; }
122
+
123
+ out(`\n${c.bold}Dostawy DeerGroup (${list.length}):${c.reset}`);
124
+ const headers = ['ID', 'Data / Miesiąc', 'Status', 'Dostawca', 'Magazyn'];
125
+ const rows = list.map(d => [
126
+ d.id ? d.id.slice(0, 8) + '…' : '',
127
+ d.month && d.year ? `${d.month}/${d.year}` : (d.delivery_date ? d.delivery_date.slice(0, 10) : '–'),
128
+ d.status || 'nowa',
129
+ d.supplier?.name || d.supplier_name || '–',
130
+ d.warehouse?.name || d.warehouse_name || '–',
131
+ ]);
132
+ out(formatTable(headers, rows));
133
+ return;
134
+ }
135
+
136
+ if (action === 'track') {
137
+ const tracking = pos[1] || flags.number;
138
+ if (!tracking) fail('Użycie: deertools deliveries track <numer_listu_lub_paczki>', 2);
139
+ const res = await apiRequest(product, { method: 'GET', path: `/deliveries-api/packages/by-tracking/${encodeURIComponent(tracking)}` });
140
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
141
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
142
+ const pkg = res.json || {};
143
+ out(`\n${c.bold}Status paczki: ${tracking}${c.reset}`);
144
+ out(` Kurier: ${pkg.courier || '–'} · Status: ${pkg.status || 'w drodze'}`);
145
+ out(` Dostawa ID: ${pkg.delivery_id || '–'}`);
146
+ return;
147
+ }
148
+
149
+ if (action === 'suppliers') {
150
+ const res = await apiRequest(product, { method: 'GET', path: '/suppliers-api/suppliers' });
151
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
152
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
153
+ const sups = res.json?.data || res.json || [];
154
+ out(`\n${c.bold}Dostawcy w Delivery Hub (${sups.length}):${c.reset}`);
155
+ for (const s of sups.slice(0, 30)) {
156
+ out(` • ${String(s.name).padEnd(30)} ${s.email || ''}`);
157
+ }
158
+ return;
159
+ }
160
+
161
+ out(`\n${c.bold}Polecenia systemu Dostaw:${c.reset}
162
+ deertools deliveries list [--status active] [--warehouse UUID] [--json]
163
+ deertools deliveries track <numer_śledzenia> [--json]
164
+ deertools deliveries suppliers [--json]
165
+ `);
166
+ }
167
+
168
+ // ==================== SUPPLIER HUB ====================
169
+ async function cmdSupplier(pos, flags) {
170
+ const action = pos[0] || 'help';
171
+ const product = getProduct('supplier');
172
+
173
+ if (action === 'discounts') {
174
+ const manufacturer = pos[1] || flags.manufacturer;
175
+ const q = {};
176
+ if (manufacturer) q.manufacturer = manufacturer;
177
+ if (flags.postal_code) q.postal_code = flags.postal_code;
178
+
179
+ const res = await apiRequest(product, { method: 'GET', path: '/discounts', query: q });
180
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
181
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
182
+ const list = res.json?.data || res.json || [];
183
+ out(`\n${c.bold}Rabaty zakupowe dostawców (${list.length}):${c.reset}`);
184
+ const headers = ['Producent', 'Dostawca', 'Rabat %', 'Kolekcja / Warunek'];
185
+ const rows = list.map(d => [
186
+ d.manufacturer_name || d.manufacturer || '–',
187
+ d.supplier_name || d.supplier || '–',
188
+ d.discount_pct != null ? `${d.discount_pct}%` : `${d.value || 0}%`,
189
+ d.collection || d.notes || '–',
190
+ ]);
191
+ out(formatTable(headers, rows));
192
+ return;
193
+ }
194
+
195
+ if (action === 'markups') {
196
+ const location = pos[1] || flags.location;
197
+ const q = {};
198
+ if (location) q.location = location;
199
+
200
+ const res = await apiRequest(product, { method: 'GET', path: '/markups', query: q });
201
+ if (!res.ok) fail(explain(res.status, res.json, product), res.status === 401 ? 3 : 1);
202
+ if (flags.json) { out(JSON.stringify(res.json, null, 2)); return; }
203
+ const list = res.json?.data || res.json || [];
204
+ out(`\n${c.bold}Narzuty sprzedaży (${list.length}):${c.reset}`);
205
+ const headers = ['Lokalizacja', 'Pakiet', 'Narzut %', 'Kategoria'];
206
+ const rows = list.map(m => [
207
+ m.location || 'ogólna',
208
+ m.package || 'wszystkie',
209
+ `${m.markup_pct != null ? m.markup_pct : (m.value || 0)}%`,
210
+ m.category || '–',
211
+ ]);
212
+ out(formatTable(headers, rows));
213
+ return;
214
+ }
215
+
216
+ if (action === 'calculate') {
217
+ const catalogPrice = Number(pos[1] || flags.price);
218
+ if (!catalogPrice || isNaN(catalogPrice)) fail('Użycie: deertools supplier calculate <cena_katalogowa_netto> [--discount 35] [--markup 25]', 2);
219
+
220
+ const discountPct = Number(pos[2] || flags.discount || 30);
221
+ const markupPct = Number(pos[3] || flags.markup || 20);
222
+
223
+ const buyNet = catalogPrice * (1 - discountPct / 100);
224
+ const sellNet = buyNet * (1 + markupPct / 100);
225
+ const marginNet = sellNet - buyNet;
226
+ const marginPct = (marginNet / sellNet) * 100;
227
+
228
+ const result = {
229
+ catalogPriceNet: catalogPrice,
230
+ discountPct,
231
+ buyPriceNet: Math.round(buyNet * 100) / 100,
232
+ markupPct,
233
+ sellPriceNet: Math.round(sellNet * 100) / 100,
234
+ marginNet: Math.round(marginNet * 100) / 100,
235
+ marginPct: Math.round(marginPct * 10) / 10,
236
+ };
237
+
238
+ if (flags.json) { out(JSON.stringify(result, null, 2)); return; }
239
+
240
+ out(`\n${c.bold}Symulacja łańcucha marżowego DeerTools:${c.reset}`);
241
+ out(` Cena katalogowa producenta: ${c.bold}${catalogPrice.toFixed(2)} zł netto${c.reset}`);
242
+ out(` Rabat zakupu dostawcy (${discountPct}%): -${(catalogPrice - buyNet).toFixed(2)} zł`);
243
+ out(` --------------------------------------------------`);
244
+ out(` Twój koszt zakupu netto: ${c.bold}${buyNet.toFixed(2)} zł netto${c.reset}`);
245
+ out(` Narzut sprzedaży (${markupPct}%): +${(sellNet - buyNet).toFixed(2)} zł`);
246
+ out(` --------------------------------------------------`);
247
+ out(` Sugerowana cena sprzedaży: ${c.green}${c.bold}${sellNet.toFixed(2)} zł netto${c.reset}`);
248
+ out(` Zysk z marży: ${c.green}${marginNet.toFixed(2)} zł (${marginPct.toFixed(1)}%)${c.reset}\n`);
249
+ return;
250
+ }
251
+
252
+ out(`\n${c.bold}Polecenia Supplier Hub:${c.reset}
253
+ deertools supplier discounts [producent] [--postal_code 00-001] [--json]
254
+ deertools supplier markups [lokalizacja] [--json]
255
+ deertools supplier calculate <cena_katalogowa> [--discount 30] [--markup 20] [--json]
256
+ `);
257
+ }
258
+
259
+ // ==================== RENDERMIND ====================
260
+ async function cmdRendermind(pos, flags) {
261
+ const action = pos[0] || 'styles';
262
+ const product = getProduct('rendermind');
263
+
264
+ if (action === 'styles') {
265
+ const res = await apiRequest(product, { method: 'GET', path: '/styles' }).catch(() => ({ ok: false }));
266
+ const defaultStyles = [
267
+ { name: 'Japandi', palette: 'Jasne drewno, beże, biel, len, ceramika' },
268
+ { name: 'Modern Classic', palette: 'Sztukateria, mosiądz, marmur, welur' },
269
+ { name: 'Loft / Industrial', palette: 'Cegła, czarna stal, surowy beton' },
270
+ { name: 'Skandynawski', palette: 'Biel, jasny dąb, pastele, naturalne światło' },
271
+ { name: 'Minimalistyczny', palette: 'Gładkie fronty, monolityczne płaszczyzny, ukryte detale' },
272
+ ];
273
+ const styles = (res.ok && res.json?.styles) ? res.json.styles : defaultStyles;
274
+ if (flags.json) { out(JSON.stringify(styles, null, 2)); return; }
275
+ out(`\n${c.bold}Dostępne style wykończeniowe RenderMind:${c.reset}`);
276
+ for (const s of styles) {
277
+ out(` • ${c.bold}${String(s.name).padEnd(20)}${c.reset} ${s.palette || s.description || ''}`);
278
+ }
279
+ return;
280
+ }
281
+
282
+ out(`\n${c.bold}Polecenia RenderMind:${c.reset}
283
+ deertools rendermind styles [--json]
284
+ deertools rendermind get <job_id> [--json]
285
+ `);
286
+ }
287
+
288
+ module.exports = { cmdCatalog, cmdDeliveries, cmdSupplier, cmdRendermind };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const isColorSupported = !process.env.NO_COLOR && (process.stdout.isTTY || process.env.FORCE_COLOR);
4
+
5
+ const c = {
6
+ reset: isColorSupported ? '\x1b[0m' : '',
7
+ bold: isColorSupported ? '\x1b[1m' : '',
8
+ dim: isColorSupported ? '\x1b[2m' : '',
9
+ green: isColorSupported ? '\x1b[32m' : '',
10
+ yellow: isColorSupported ? '\x1b[33m' : '',
11
+ red: isColorSupported ? '\x1b[31m' : '',
12
+ cyan: isColorSupported ? '\x1b[36m' : '',
13
+ blue: isColorSupported ? '\x1b[34m' : '',
14
+ gray: isColorSupported ? '\x1b[90m' : '',
15
+ magenta: isColorSupported ? '\x1b[35m' : '',
16
+ };
17
+
18
+ function formatTable(headers, rows) {
19
+ if (!rows || rows.length === 0) return '(brak danych)';
20
+ const colWidths = headers.map((h, i) => {
21
+ let max = h.length;
22
+ for (const r of rows) {
23
+ const val = String(r[i] != null ? r[i] : '');
24
+ if (val.length > max) max = val.length;
25
+ }
26
+ return Math.min(max, 60);
27
+ });
28
+
29
+ const sep = colWidths.map(w => '─'.repeat(w)).join('─┼─');
30
+ const top = colWidths.map(w => '─'.repeat(w)).join('─┬─');
31
+ const bot = colWidths.map(w => '─'.repeat(w)).join('─┴─');
32
+
33
+ const headStr = headers.map((h, i) => `${c.bold}${h.padEnd(colWidths[i])}${c.reset}`).join(' │ ');
34
+ const rowStrs = rows.map(r => {
35
+ return r.map((val, i) => {
36
+ const s = String(val != null ? val : '');
37
+ return s.length > colWidths[i] ? s.slice(0, colWidths[i] - 1) + '…' : s.padEnd(colWidths[i]);
38
+ }).join(' │ ');
39
+ });
40
+
41
+ return [
42
+ `┌─${top}─┐`,
43
+ `│ ${headStr} │`,
44
+ `├─${sep}─┤`,
45
+ ...rowStrs.map(r => `│ ${r} │`),
46
+ `└─${bot}─┘`,
47
+ ].join('\n');
48
+ }
49
+
50
+ module.exports = { c, formatTable };