@deerdesign/deertools 1.2.5 → 1.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deerdesign/deertools",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
package/src/cli.js CHANGED
@@ -35,6 +35,7 @@ async function main(argv) {
35
35
  mcp: c.cmdMcp, doctor: c.cmdDoctor, audit: c.cmdAudit, profile: c.cmdProfile,
36
36
  completion: c.cmdCompletion, catalog: c.cmdCatalog, deliveries: c.cmdDeliveries,
37
37
  supplier: c.cmdSupplier, rendermind: c.cmdRendermind, baselinker: c.cmdBaseLinker,
38
+ update: require('./updater').cmdUpdate,
38
39
  };
39
40
 
40
41
  if (pos.length === 0 && !flags.help && !flags.version && process.stdout.isTTY) {
@@ -48,15 +49,28 @@ async function main(argv) {
48
49
 
49
50
  const cmd = pos.shift() || 'help';
50
51
 
51
- if (cmd === 'help' || cmd === '--help' || flags.help) { process.stdout.write(c.HELP); return 0; }
52
- if (cmd === '--version' || cmd === 'version' || flags.version) { process.stdout.write(`${require('../package.json').version}\n`); return 0; }
53
- if (!table[cmd]) { process.stderr.write(`Nieznana komenda „${cmd}”.\n${c.HELP}`); return 2; }
52
+ const shouldCheck = !flags.json && cmd !== 'update' && process.env.NODE_ENV !== 'test';
53
+ const updatePromise = shouldCheck ? require('./updater').checkUpdate().catch(() => null) : null;
54
+
55
+ const finish = async (code = 0) => {
56
+ if (updatePromise) {
57
+ try {
58
+ const info = await updatePromise;
59
+ if (info && info.hasUpdate) require('./updater').printUpdateNotice(info);
60
+ } catch (_) {}
61
+ }
62
+ return code;
63
+ };
64
+
65
+ if (cmd === 'help' || cmd === '--help' || flags.help) { process.stdout.write(c.HELP); return finish(0); }
66
+ if (cmd === '--version' || cmd === 'version' || flags.version) { process.stdout.write(`${require('../package.json').version}\n`); return finish(0); }
67
+ if (!table[cmd]) { process.stderr.write(`Nieznana komenda „${cmd}”.\n${c.HELP}`); return finish(2); }
54
68
  try {
55
69
  await table[cmd](pos, flags);
56
- return 0;
70
+ return finish(0);
57
71
  } catch (e) {
58
72
  if (!e.silent) process.stderr.write(`Błąd: ${e.message}\n`);
59
- return e.exitCode || 1;
73
+ return finish(e.exitCode || 1);
60
74
  }
61
75
  }
62
76
 
package/src/commands.js CHANGED
@@ -543,6 +543,7 @@ Integracje MCP i Agenci AI (DeerSkills Hub):
543
543
  deertools api <produkt> <METODA> </ścieżka> Wywołuje bezpośrednio API produktu.
544
544
 
545
545
  Narzędzia systemowe:
546
+ deertools update Sprawdza i podpowiada aktualizację CLI.
546
547
  deertools doctor [--json] Pełna diagnostyka połączeń, portów i kluczy.
547
548
  deertools audit [--limit 25] [--clear] [--json] Historia operacji i wywołań API.
548
549
  deertools completion [install|zsh|bash] Autouzupełnianie w powłoce po wciśnięciu Tab.
package/src/login.js CHANGED
@@ -33,9 +33,9 @@ function normalizeScope(input) {
33
33
  return VALID_SCOPES.filter((s) => set.has(s)).join(' ');
34
34
  }
35
35
 
36
- function buildAuthorizeUrl(authorizationEndpoint, { redirectUri, state, scope, clientName, challenge }) {
36
+ function buildAuthorizeUrl(authorizationEndpoint, { redirectUri, state, scope, clientName, challenge, clientId = CLIENT_ID }) {
37
37
  const u = new URL(authorizationEndpoint);
38
- u.searchParams.set('client_id', CLIENT_ID);
38
+ u.searchParams.set('client_id', clientId);
39
39
  u.searchParams.set('redirect_uri', redirectUri);
40
40
  u.searchParams.set('response_type', 'code');
41
41
  u.searchParams.set('code_challenge', challenge);
@@ -67,13 +67,13 @@ function evaluateCallback(query, { expectedState }) {
67
67
  }
68
68
 
69
69
  /** Wymiana kodu na klucz: bezpośrednie zapytanie CLI -> serwer (klucz nigdy nie przechodzi przez przeglądarkę). */
70
- async function exchangeCode(product, tokenEndpoint, { code, verifier, redirectUri, fetchImpl = fetch }) {
70
+ async function exchangeCode(product, tokenEndpoint, { code, verifier, redirectUri, clientId = CLIENT_ID, fetchImpl = fetch }) {
71
71
  let res;
72
72
  try {
73
73
  res = await fetchImpl(tokenEndpoint, {
74
74
  method: 'POST',
75
75
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
76
- body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: verifier, redirect_uri: redirectUri, client_id: CLIENT_ID }),
76
+ body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: verifier, redirect_uri: redirectUri, client_id: clientId }),
77
77
  });
78
78
  } catch (e) {
79
79
  throw new Error(`Nie udało się połączyć z serwerem produktu ${product.name}: ${e.message}`);
@@ -162,6 +162,28 @@ function openBrowser(url) {
162
162
  exec(cmd, () => { /* gdy się nie uda, użytkownik ma adres wypisany w terminalu */ });
163
163
  }
164
164
 
165
+ async function registerDynamicClient(registrationEndpoint, { redirectUri, clientName, fetchImpl = fetch }) {
166
+ if (!registrationEndpoint) return null;
167
+ try {
168
+ const res = await fetchImpl(registrationEndpoint, {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
171
+ body: JSON.stringify({
172
+ client_name: clientName || 'DeerTools CLI',
173
+ redirect_uris: [redirectUri],
174
+ token_endpoint_auth_method: 'none',
175
+ grant_types: ['authorization_code', 'refresh_token'],
176
+ response_types: ['code'],
177
+ }),
178
+ });
179
+ if (!res.ok) return null;
180
+ const json = await res.json();
181
+ return json.client_id || null;
182
+ } catch (_) {
183
+ return null;
184
+ }
185
+ }
186
+
165
187
  /**
166
188
  * Logowanie OAuth 2.1 dla aplikacji natywnej (RFC 8252): kod autoryzacyjny + PKCE, powrót na 127.0.0.1 (losowy port).
167
189
  * opts.metadata pozwala podać adresy bez pobierania (testy). Zwraca wpis poświadczeń zapisany na dysku.
@@ -179,6 +201,7 @@ async function login(product, opts = {}) {
179
201
  return new Promise((resolve, reject) => {
180
202
  let finished = false;
181
203
  let redirectUri = null;
204
+ let activeClientId = CLIENT_ID;
182
205
 
183
206
  const server = http.createServer(async (req, res) => {
184
207
  const url = new URL(req.url, 'http://127.0.0.1');
@@ -192,7 +215,7 @@ async function login(product, opts = {}) {
192
215
  return;
193
216
  }
194
217
  try {
195
- const entry = await exchangeCode(product, meta.token_endpoint, { code: result.code, verifier: pkce.verifier, redirectUri, fetchImpl: opts.fetchImpl });
218
+ const entry = await exchangeCode(product, meta.token_endpoint, { code: result.code, verifier: pkce.verifier, redirectUri, clientId: activeClientId, fetchImpl: opts.fetchImpl });
196
219
  credentials.saveProduct(product.id, entry);
197
220
  res.writeHead(200, headers);
198
221
  res.end(page('Zalogowano', `Klucz do produktu ${product.name} został zapisany w terminalu. Możesz zamknąć to okno.`, true));
@@ -215,9 +238,17 @@ async function login(product, opts = {}) {
215
238
  }
216
239
 
217
240
  server.on('error', (e) => done(e));
218
- server.listen(opts.port || 0, '127.0.0.1', () => {
241
+ server.listen(opts.port || 0, '127.0.0.1', async () => {
219
242
  redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
220
- const url = buildAuthorizeUrl(meta.authorization_endpoint, { redirectUri, state, scope, clientName, challenge: pkce.challenge });
243
+ let authScope = scope;
244
+ if (meta.registration_endpoint) {
245
+ const dynId = await registerDynamicClient(meta.registration_endpoint, { redirectUri, clientName, fetchImpl: opts.fetchImpl });
246
+ if (dynId) {
247
+ activeClientId = dynId;
248
+ authScope = 'openid email profile offline_access';
249
+ }
250
+ }
251
+ const url = buildAuthorizeUrl(meta.authorization_endpoint, { redirectUri, state, scope: authScope, clientName, challenge: pkce.challenge, clientId: activeClientId });
221
252
  log(`Zakres: ${scope}. Otwieram przeglądarkę — zaloguj się w produkcie ${product.name} i kliknij „Zezwól na dostęp”.`);
222
253
  log(`Jeśli przeglądarka się nie otworzy, wejdź na:\n${url}\n`);
223
254
  if (open) open(url);
@@ -225,4 +256,4 @@ async function login(product, opts = {}) {
225
256
  });
226
257
  }
227
258
 
228
- module.exports = { CLIENT_ID, createPkce, normalizeScope, buildAuthorizeUrl, evaluateCallback, exchangeCode, discoverOAuth, login };
259
+ module.exports = { CLIENT_ID, createPkce, normalizeScope, buildAuthorizeUrl, evaluateCallback, exchangeCode, discoverOAuth, login, registerDynamicClient };
package/src/updater.js ADDED
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const CACHE_TTL_MS = 12 * 60 * 60 * 1000; // 12 hours
7
+ const REGISTRY_URL = 'https://registry.npmjs.org/@deerdesign/deertools/latest';
8
+ const PKG = require('../package.json');
9
+
10
+ function getCachePath() {
11
+ const dir = path.join(os.homedir(), '.deertools');
12
+ try {
13
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
14
+ } catch (_) {}
15
+ return path.join(dir, 'update-check.json');
16
+ }
17
+
18
+ function compareSemver(a, b) {
19
+ const pa = String(a).replace(/^v/, '').split('.').map(Number);
20
+ const pb = String(b).replace(/^v/, '').split('.').map(Number);
21
+ for (let i = 0; i < 3; i++) {
22
+ const na = pa[i] || 0;
23
+ const nb = pb[i] || 0;
24
+ if (na > nb) return 1;
25
+ if (na < nb) return -1;
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ async function fetchLatestVersion() {
31
+ const ctrl = new AbortController();
32
+ const t = setTimeout(() => ctrl.abort(), 2500);
33
+ try {
34
+ const res = await fetch(REGISTRY_URL, {
35
+ headers: { 'Accept': 'application/json', 'User-Agent': 'deertools-cli' },
36
+ signal: ctrl.signal,
37
+ });
38
+ if (!res.ok) return null;
39
+ const json = await res.json();
40
+ return json.version || null;
41
+ } catch (_) {
42
+ return null;
43
+ } finally {
44
+ clearTimeout(t);
45
+ }
46
+ }
47
+
48
+ async function checkUpdate() {
49
+ const cacheFile = getCachePath();
50
+ const now = Date.now();
51
+ let cached = null;
52
+
53
+ try {
54
+ if (fs.existsSync(cacheFile)) {
55
+ cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
56
+ }
57
+ } catch (_) {}
58
+
59
+ let latestVersion = cached && cached.latestVersion ? cached.latestVersion : null;
60
+
61
+ if (!cached || !cached.lastChecked || (now - cached.lastChecked > CACHE_TTL_MS)) {
62
+ const fetched = await fetchLatestVersion();
63
+ if (fetched) {
64
+ latestVersion = fetched;
65
+ try {
66
+ fs.writeFileSync(cacheFile, JSON.stringify({ lastChecked: now, latestVersion }), 'utf8');
67
+ } catch (_) {}
68
+ }
69
+ }
70
+
71
+ if (latestVersion && compareSemver(latestVersion, PKG.version) > 0) {
72
+ return {
73
+ hasUpdate: true,
74
+ current: PKG.version,
75
+ latest: latestVersion,
76
+ };
77
+ }
78
+
79
+ return { hasUpdate: false, current: PKG.version, latest: latestVersion };
80
+ }
81
+
82
+ function printUpdateNotice(updateInfo) {
83
+ if (!updateInfo || !updateInfo.hasUpdate) return;
84
+ const { current, latest } = updateInfo;
85
+ const line1 = `Dostępna nowa wersja DeerTools CLI: ${current} → ${latest}`;
86
+ const line2 = `Zaktualizuj: npm install -g @deerdesign/deertools@latest`;
87
+ const line3 = `lub użyj: npx -y @deerdesign/deertools@latest <komenda>`;
88
+ const len = Math.max(line1.length, line2.length, line3.length) + 4;
89
+ const border = '─'.repeat(len);
90
+
91
+ process.stderr.write(`\n\x1b[33m┌${border}┐\x1b[0m\n`);
92
+ process.stderr.write(`\x1b[33m│ ${line1.padEnd(len - 2)}│\x1b[0m\n`);
93
+ process.stderr.write(`\x1b[33m│ ${line2.padEnd(len - 2)}│\x1b[0m\n`);
94
+ process.stderr.write(`\x1b[33m│ ${line3.padEnd(len - 2)}│\x1b[0m\n`);
95
+ process.stderr.write(`\x1b[33m└${border}┘\x1b[0m\n\n`);
96
+ }
97
+
98
+ async function cmdUpdate(pos, flags) {
99
+ process.stdout.write(`Sprawdzam aktualizacje w rejestrze npm...\n`);
100
+ const latest = await fetchLatestVersion();
101
+ if (!latest) {
102
+ process.stdout.write(`Nie udało się połączyć z rejestrem npm. Sprawdź połączenie z internetem.\n`);
103
+ return 1;
104
+ }
105
+
106
+ const cmp = compareSemver(latest, PKG.version);
107
+ if (cmp <= 0) {
108
+ process.stdout.write(`✓ DeerTools CLI jest w najnowszej wersji (${PKG.version}).\n`);
109
+ return 0;
110
+ }
111
+
112
+ process.stdout.write(`\nZnaleziono nowszą wersję: ${PKG.version} → \x1b[32m${latest}\x1b[0m\n\n`);
113
+ process.stdout.write(`Aby zaktualizować instalację globalną, uruchom:\n`);
114
+ process.stdout.write(` \x1b[36mnpm install -g @deerdesign/deertools@latest\x1b[0m\n\n`);
115
+ process.stdout.write(`Jeśli instalacja wymaga uprawnień administratora:\n`);
116
+ process.stdout.write(` \x1b[36msudo npm install -g @deerdesign/deertools@latest\x1b[0m\n\n`);
117
+ process.stdout.write(`A jeśli korzystasz z npx, każda komenda zawsze pobiera najnowszą wersję z tagiem @latest:\n`);
118
+ process.stdout.write(` \x1b[36mnpx -y @deerdesign/deertools@latest <komenda>\x1b[0m\n\n`);
119
+ return 0;
120
+ }
121
+
122
+ module.exports = {
123
+ checkUpdate,
124
+ printUpdateNotice,
125
+ cmdUpdate,
126
+ compareSemver,
127
+ fetchLatestVersion,
128
+ };