@deerdesign/deertools 1.2.26 → 1.2.31

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.
@@ -6,6 +6,7 @@ const skillsHub = require('./skills-hub');
6
6
  const { parseFrontmatterName } = require('./openapi');
7
7
  const { resolveProducts } = require('./registry');
8
8
  const { fetchText } = require('./http');
9
+ const embedding = require('./embedding');
9
10
 
10
11
  const { compareSemver } = require('./updater');
11
12
 
@@ -92,6 +93,82 @@ function readSkillVersion(filePath, lock, skill) {
92
93
  return '1.0.0';
93
94
  }
94
95
 
96
+ /**
97
+ * Skanuje lokalne katalogi w poszukiwaniu skilli na dysku.
98
+ * Zwraca mapę slug -> { slug, version, dir, filePath, summary }.
99
+ */
100
+ function scanLocalSkills(cwd = process.cwd(), agents = [], hubSlugs = null) {
101
+ const localSkills = new Map();
102
+
103
+ function inspectDir(skillDir, isAgentDir = false) {
104
+ const skillMd = path.join(skillDir, 'SKILL.md');
105
+ if (!fs.existsSync(skillMd)) return;
106
+ try {
107
+ const content = fs.readFileSync(skillMd, 'utf8');
108
+ let slug = path.basename(skillDir);
109
+ let version = '1.0.0';
110
+ let summary = '';
111
+ if (content.startsWith('---')) {
112
+ const parts = content.split('---', 2);
113
+ if (parts.length >= 2) {
114
+ const nameMatch = parts[1].match(/^name:\s*(.+)$/m);
115
+ if (nameMatch) slug = nameMatch[1].trim();
116
+ const verMatch = parts[1].match(/^version:\s*['"]?([0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.]+)?)/im);
117
+ if (verMatch) version = verMatch[1].trim();
118
+ const descMatch = parts[1].match(/^description:\s*(.+)$/m);
119
+ if (descMatch) summary = descMatch[1].trim();
120
+ }
121
+ }
122
+
123
+ // Jeśli skanujemy katalog zewnętrznego agenta (~/.gemini/...): bierzemy tylko skille Deer Design lub znane w Hubie
124
+ if (isAgentDir) {
125
+ const isDeer = slug.startsWith('deer') || slug.startsWith('rendermind') || slug.startsWith('@');
126
+ const isKnown = hubSlugs && (hubSlugs.has(slug) || hubSlugs.has(slug.replace(/^@[^/]+\//, '')));
127
+ if (!isDeer && !isKnown) return;
128
+ }
129
+
130
+ if (!localSkills.has(slug)) {
131
+ localSkills.set(slug, {
132
+ slug,
133
+ version,
134
+ dir: skillDir,
135
+ filePath: skillMd,
136
+ summary,
137
+ });
138
+ }
139
+ } catch (_) {}
140
+ }
141
+
142
+ // 1. Sprawdź .agents/skills w bieżącym projekcie
143
+ const projectAgentsSkills = path.join(cwd, '.agents', 'skills');
144
+ if (fs.existsSync(projectAgentsSkills)) {
145
+ try {
146
+ const entries = fs.readdirSync(projectAgentsSkills, { withFileTypes: true });
147
+ for (const ent of entries) {
148
+ if (ent.isDirectory() && !ent.name.startsWith('.')) {
149
+ inspectDir(path.join(projectAgentsSkills, ent.name), false);
150
+ }
151
+ }
152
+ } catch (_) {}
153
+ }
154
+
155
+ // 2. Sprawdź katalogi wykrytych agentów (tylko skille powiązane z Hubem lub Deer Design)
156
+ for (const agent of agents) {
157
+ if (agent.targetDir && fs.existsSync(agent.targetDir)) {
158
+ try {
159
+ const entries = fs.readdirSync(agent.targetDir, { withFileTypes: true });
160
+ for (const ent of entries) {
161
+ if (ent.isDirectory() && !ent.name.startsWith('.')) {
162
+ inspectDir(path.join(agent.targetDir, ent.name), true);
163
+ }
164
+ }
165
+ } catch (_) {}
166
+ }
167
+ }
168
+
169
+ return localSkills;
170
+ }
171
+
95
172
  /**
96
173
  * Bada stan instalacji i wersję skilla u wszystkich zadanych agentów.
97
174
  */
@@ -101,6 +178,7 @@ function inspectSkillForAgents(agents, skill, lock) {
101
178
  let upToDateCount = 0;
102
179
  let outdatedCount = 0;
103
180
  let missingCount = 0;
181
+ let aheadCount = 0;
104
182
 
105
183
  for (const agent of agents) {
106
184
  const filePath = findSkillFileForAgent(agent, skill);
@@ -122,7 +200,7 @@ function inspectSkillForAgents(agents, skill, lock) {
122
200
  const installedVer = readSkillVersion(filePath, lock, skill);
123
201
  const cmp = compareSemver(installedVer, skill.latest_version);
124
202
 
125
- if (cmp >= 0) {
203
+ if (cmp === 0) {
126
204
  agentStatuses[agent.id] = {
127
205
  agentId: agent.id,
128
206
  agentName: agent.name,
@@ -133,6 +211,19 @@ function inspectSkillForAgents(agents, skill, lock) {
133
211
  badge: `\x1b[32m[✓ ${agent.id}: v${installedVer}]\x1b[0m`,
134
212
  };
135
213
  upToDateCount += 1;
214
+ } else if (cmp > 0) {
215
+ agentStatuses[agent.id] = {
216
+ agentId: agent.id,
217
+ agentName: agent.name,
218
+ installed: true,
219
+ version: installedVer,
220
+ status: 'ahead',
221
+ hub_version: skill.latest_version,
222
+ filePath,
223
+ badge: `\x1b[35m[★ ${agent.id}: v${installedVer} > Hub v${skill.latest_version}]\x1b[0m`,
224
+ };
225
+ upToDateCount += 1;
226
+ aheadCount += 1;
136
227
  } else {
137
228
  agentStatuses[agent.id] = {
138
229
  agentId: agent.id,
@@ -152,7 +243,10 @@ function inspectSkillForAgents(agents, skill, lock) {
152
243
  let overallBadge = '\x1b[90m[○ Niezainstalowany]\x1b[0m';
153
244
 
154
245
  if (installedCount > 0) {
155
- if (installedCount === agents.length && outdatedCount === 0) {
246
+ if (aheadCount > 0) {
247
+ overallStatus = 'ahead';
248
+ overallBadge = `\x1b[35m[★ Nowszy lokalnie (${aheadCount})]\x1b[0m`;
249
+ } else if (installedCount === agents.length && outdatedCount === 0) {
156
250
  overallStatus = 'up_to_date';
157
251
  overallBadge = `\x1b[32m[✓ Aktualny (${installedCount}/${agents.length})]\x1b[0m`;
158
252
  } else if (outdatedCount > 0) {
@@ -170,27 +264,148 @@ function inspectSkillForAgents(agents, skill, lock) {
170
264
  upToDateCount,
171
265
  outdatedCount,
172
266
  missingCount,
267
+ aheadCount,
173
268
  overallStatus,
174
269
  overallBadge,
175
270
  };
176
271
  }
177
272
 
273
+ /**
274
+ * Sprawdza czy dany skill pasuje do zapytania tekstowego.
275
+ * Przeszukuje kompleksowo:
276
+ * 1. Nazwę (slug, full_slug, title)
277
+ * 2. Opis (summary / description - pełny tekst opisu z SKILL.md)
278
+ * 3. Tagi (zarówno całe tagi, jak i dopasowania cząstkowe, np. 'matrycowy' w 'cennik-matrycowy')
279
+ * 4. Kategorie (nazwa kategorii oraz jej slug)
280
+ * 5. Namespace oraz autora (owner_name, owner_email)
281
+ */
282
+ function matchSkillQuery(skill, query) {
283
+ if (!query) return true;
284
+ const q = String(query).toLowerCase().trim();
285
+ if (!q) return true;
286
+
287
+ // 1. Nazwa, slug, full_slug, title
288
+ if (skill.slug && String(skill.slug).toLowerCase().includes(q)) return true;
289
+ if (skill.full_slug && String(skill.full_slug).toLowerCase().includes(q)) return true;
290
+ if (skill.title && String(skill.title).toLowerCase().includes(q)) return true;
291
+
292
+ // 2. Opis (summary / description)
293
+ if (skill.summary && String(skill.summary).toLowerCase().includes(q)) return true;
294
+ if (skill.description && String(skill.description).toLowerCase().includes(q)) return true;
295
+
296
+ // 3. Tagi (zarówno dopasowanie pełne, jak i podciąg wewnątrz tagu)
297
+ if (Array.isArray(skill.tags)) {
298
+ for (const tag of skill.tags) {
299
+ if (String(tag).toLowerCase().includes(q)) return true;
300
+ }
301
+ }
302
+
303
+ // 4. Kategoria
304
+ if (skill.categories) {
305
+ if (skill.categories.name && String(skill.categories.name).toLowerCase().includes(q)) return true;
306
+ if (skill.categories.slug && String(skill.categories.slug).toLowerCase().includes(q)) return true;
307
+ }
308
+ if (skill.category && typeof skill.category === 'string' && skill.category.toLowerCase().includes(q)) return true;
309
+
310
+ // 5. Namespace i właściciel
311
+ if (skill.namespace && String(skill.namespace).toLowerCase().includes(q)) return true;
312
+ if (skill.owner_name && String(skill.owner_name).toLowerCase().includes(q)) return true;
313
+ if (skill.owner_email && String(skill.owner_email).toLowerCase().includes(q)) return true;
314
+
315
+ return false;
316
+ }
317
+
178
318
  async function cmdSkillList(flags, out, err) {
179
- if (!flags.json) out('Pobieram listę skilli z DeerSkills Hub...');
180
- const res = await skillsHub.listSkills({
181
- query: flags.query,
182
- tag: flags.tag,
183
- category: flags.category,
184
- scope: flags.scope,
185
- limit: flags.limit || 100,
186
- });
319
+ const searchQuery = flags.query ? String(flags.query).trim() : '';
320
+ const isSearch = Boolean(searchQuery);
187
321
 
188
- if (!res.ok) {
189
- err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
190
- return;
322
+ if (!flags.json) {
323
+ if (isSearch) {
324
+ out(`Wyszukuję skille w DeerSkills Hub dla frazy „${searchQuery}” (hybrydowo: wektory + tekst)...`);
325
+ } else {
326
+ out('Pobieram listę skilli z DeerSkills Hub...');
327
+ }
328
+ }
329
+
330
+ let items = [];
331
+
332
+ if (isSearch) {
333
+ // 1. Serwerowe wyszukiwanie wektorowo-hybrydowe (PostgreSQL pgvector + trigram + keyword boost)
334
+ let queryEmbedding = null;
335
+ if (flags.semantic) {
336
+ if (!flags.json) out('Generuję wektor semantyczny zapytania...');
337
+ queryEmbedding = embedding.computeEmbedding(searchQuery);
338
+ }
339
+
340
+ const hybridRes = await skillsHub.searchSkillsHybrid({
341
+ queryText: searchQuery,
342
+ queryEmbedding,
343
+ threshold: queryEmbedding ? 0.1 : 0.05,
344
+ count: flags.limit || 50,
345
+ });
346
+
347
+ if (hybridRes.ok && Array.isArray(hybridRes.data)) {
348
+ items = hybridRes.data;
349
+
350
+ // 2. Jeśli zapytanie tekstowe dało 0 wyników, a dostępne jest lokalne generowanie wektorów — ponów semantycznie
351
+ if (items.length === 0 && !flags.semantic && embedding.isEmbeddingAvailable()) {
352
+ const autoEmb = embedding.computeEmbedding(searchQuery);
353
+ if (autoEmb) {
354
+ const semRes = await skillsHub.searchSkillsHybrid({
355
+ queryText: searchQuery,
356
+ queryEmbedding: autoEmb,
357
+ threshold: 0.12,
358
+ count: flags.limit || 50,
359
+ });
360
+ if (semRes.ok && Array.isArray(semRes.data) && semRes.data.length > 0) {
361
+ items = semRes.data;
362
+ }
363
+ }
364
+ }
365
+
366
+ if (flags.tag) {
367
+ items = items.filter((s) => Array.isArray(s.tags) && s.tags.includes(flags.tag));
368
+ }
369
+ if (flags.category) {
370
+ const cat = String(flags.category).toLowerCase();
371
+ items = items.filter((s) => s.category_slug === cat || (s.category_name && s.category_name.toLowerCase().includes(cat)));
372
+ }
373
+ if (flags.scope) {
374
+ items = items.filter((s) => s.scope === flags.scope);
375
+ }
376
+ } else {
377
+ // Fallback do pobierania pełnej listy i lokalnego przeszukiwania
378
+ const res = await skillsHub.listSkills({
379
+ tag: flags.tag,
380
+ category: flags.category,
381
+ scope: flags.scope,
382
+ limit: flags.limit || 200,
383
+ });
384
+
385
+ if (!res.ok) {
386
+ err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
387
+ return;
388
+ }
389
+
390
+ const allItems = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
391
+ items = allItems.filter((s) => matchSkillQuery(s, searchQuery));
392
+ }
393
+ } else {
394
+ const res = await skillsHub.listSkills({
395
+ tag: flags.tag,
396
+ category: flags.category,
397
+ scope: flags.scope,
398
+ limit: flags.limit || 200,
399
+ });
400
+
401
+ if (!res.ok) {
402
+ err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
403
+ return;
404
+ }
405
+
406
+ items = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
191
407
  }
192
408
 
193
- const items = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
194
409
  const agents = detectAgents({
195
410
  dir: flags.dir,
196
411
  agent: flags.agent,
@@ -203,6 +418,9 @@ async function cmdSkillList(flags, out, err) {
203
418
  const insp = inspectSkillForAgents(agents, s, lock);
204
419
  return {
205
420
  ...s,
421
+ similarity: s.similarity != null ? s.similarity : null,
422
+ vector_score: s.vector_score != null ? s.vector_score : null,
423
+ text_score: s.text_score != null ? s.text_score : null,
206
424
  overall_status: insp.overallStatus,
207
425
  agents: Object.fromEntries(
208
426
  Object.entries(insp.agents).map(([id, data]) => [
@@ -234,9 +452,22 @@ async function cmdSkillList(flags, out, err) {
234
452
  };
235
453
  }
236
454
 
237
- out(`\n=== Dostępne Skille w DeerSkills Hub (${items.length}) ===\n`);
455
+ const titleHeader = isSearch
456
+ ? `=== Wyniki wyszukiwania w DeerSkills Hub dla „${searchQuery}” (${items.length}) ===`
457
+ : `=== Dostępne Skille w DeerSkills Hub (${items.length}) ===`;
458
+ out(`\n${titleHeader}\n`);
459
+
460
+ if (items.length === 0 && isSearch) {
461
+ out(` Nie znaleziono żadnego skilla pasującego do frazy „${searchQuery}”.`);
462
+ out(` (Przeszukano: wektory semantyczne, nazwy/slugi, opisy [summary], tagi oraz kategorie).`);
463
+ out(`\n💡 Wskazówka: Uruchom \x1b[36mdeertools skill list\x1b[0m aby zobaczyć wszystkie dostępne skille.`);
464
+ out('─'.repeat(78) + '\n');
465
+ return;
466
+ }
467
+
238
468
  for (const s of items) {
239
- const badge = s.scope === 'global' ? '★ GLOBAL' : `@${s.namespace}`;
469
+ const namespace = s.namespace || (s.full_slug?.startsWith('@') ? s.full_slug.split('/')[0].slice(1) : '');
470
+ const badge = s.scope === 'global' ? '★ GLOBAL' : (namespace ? `@${namespace}` : (s.scope === 'team' ? '[Team]' : '[User]'));
240
471
  const latestVer = s.latest_version ? `v${s.latest_version}` : 'v1.0.0';
241
472
  const insp = inspectSkillForAgents(agents, s, lock);
242
473
 
@@ -248,8 +479,12 @@ async function cmdSkillList(flags, out, err) {
248
479
  }
249
480
 
250
481
  const agentBadges = agents.map((a) => insp.agents[a.id].badge).join(' ');
482
+ const simBadge = (s.similarity != null && isSearch)
483
+ ? `\x1b[36m[Trafność: ${Math.round(s.similarity * 100)}%]\x1b[0m`
484
+ : '';
251
485
 
252
- out(` • ${s.full_slug.padEnd(36)} ${latestVer.padEnd(10)} ${badge.padEnd(14)} ${insp.overallBadge}`);
486
+ const fullSlug = s.full_slug || s.slug || '';
487
+ out(` • ${fullSlug.padEnd(36)} ${latestVer.padEnd(10)} ${badge.padEnd(14)} ${insp.overallBadge}${simBadge ? ' ' + simBadge : ''}`);
253
488
  out(` Agenci: ${agentBadges}`);
254
489
  if (s.summary) out(` ${s.summary.slice(0, 95)}...`);
255
490
  if (s.tags && s.tags.length) out(` Tagi: ${s.tags.join(', ')}`);
@@ -279,9 +514,17 @@ async function cmdSkillList(flags, out, err) {
279
514
  out(` • ${(a.name + ' (' + a.id + '):').padEnd(36)} ${badgeText}`);
280
515
  }
281
516
 
517
+ const anyAhead = items.some((s) => {
518
+ const insp = inspectSkillForAgents(agents, s, lock);
519
+ return insp.overallStatus === 'ahead';
520
+ });
521
+
282
522
  if (anyOutdated || anyMissing) {
283
523
  out(`\n💡 Aby zainstalować brakujące i zaktualizować wszystkie skille u agentów:`);
284
524
  out(` \x1b[36mdeertools skill sync\x1b[0m (lub: \x1b[36mdeertools sync\x1b[0m)`);
525
+ } else if (anyAhead) {
526
+ out(`\n💡 Wykryto skille nowsze lokalnie niż w Hubie:`);
527
+ out(` \x1b[35mdeertools sync --push\x1b[0m (aby wypchnąć je do DeerSkills Hub)`);
285
528
  } else {
286
529
  out(`\n✓ Wszystkie skille zespołu (${items.length}) są zainstalowane i aktualne u wszystkich wykrytych agentów.`);
287
530
  }
@@ -327,6 +570,18 @@ async function installSingleSkill(slug, requestedVersion, flags, out, err, { sil
327
570
 
328
571
  let count = 0;
329
572
  for (const agent of agents) {
573
+ // Bezpieczeństwo: jeśli lokalna wersja na dysku jest nowsza niż w Hubie, chroń przed nadpisaniem (wymagaj --force)
574
+ const existingFile = findSkillFileForAgent(agent, skill);
575
+ if (existingFile && !flags.force && !requestedVersion) {
576
+ const currentDiskVer = readSkillVersion(existingFile, readLock(flags.dir), skill);
577
+ if (currentDiskVer && compareSemver(currentDiskVer, versionData.version) > 0) {
578
+ if (!silentSummary) {
579
+ out(` \x1b[35m★ [${agent.id}]\x1b[0m Pominięto instalację ${skill.slug}: wersja na dysku (v${currentDiskVer}) jest nowsza niż w Hubie (v${versionData.version}). Użyj --force aby nadpisać.`);
580
+ }
581
+ continue;
582
+ }
583
+ }
584
+
330
585
  try {
331
586
  const dest = installSkillContent(agent, skill.slug, versionData.skill_md_content);
332
587
  if (!silentSummary) {
@@ -372,8 +627,10 @@ async function installSingleSkill(slug, requestedVersion, flags, out, err, { sil
372
627
 
373
628
  /**
374
629
  * Instaluje lub synchronizuje wszystkie skille zespołu z DeerSkills Hub.
630
+ * Wspiera flagę --push (lub --auto-push): automatycznie publikuje nowsze lokalne wersje do Hubu.
375
631
  */
376
632
  async function cmdSkillSync(flags, out, err) {
633
+ const shouldPush = Boolean(flags.push || flags['auto-push']);
377
634
  out('Pobieram pełną listę skilli zespołu z DeerSkills Hub...');
378
635
  const res = await skillsHub.listSkills({ limit: 100 });
379
636
  if (!res.ok) {
@@ -399,14 +656,20 @@ async function cmdSkillSync(flags, out, err) {
399
656
  return;
400
657
  }
401
658
 
402
- const lock = readLock(flags.dir);
659
+ const cwd = flags.dir || process.cwd();
660
+ const lock = readLock(cwd);
661
+ const hubSlugs = new Set(items.flatMap((s) => [s.slug, s.full_slug, s.slug.replace(/^@[^/]+\//, '')]));
662
+ const localSkills = scanLocalSkills(cwd, agents, hubSlugs);
663
+
403
664
  out(`Platformy docelowe: \x1b[36m${agents.map((a) => a.name || a.id).join(', ')}\x1b[0m`);
404
- out(`Rozpoczynam synchronizację ${items.length} skilli zespołu...\n`);
665
+ out(`Rozpoczynam synchronizację ${items.length} skilli zespołu...${shouldPush ? ' \x1b[35m[Tryb dwukierunkowy: --push]\x1b[0m' : ''}\n`);
405
666
 
406
667
  let installedCount = 0;
407
668
  let updatedCount = 0;
408
669
  let unchangedCount = 0;
409
670
  let failedCount = 0;
671
+ let pushedCount = 0;
672
+ const aheadSkills = [];
410
673
 
411
674
  for (const s of items) {
412
675
  const slug = s.slug;
@@ -415,16 +678,74 @@ async function cmdSkillSync(flags, out, err) {
415
678
  const currentVer = current?.version;
416
679
  const latestVer = s.latest_version || '1.0.0';
417
680
 
418
- if (currentVer && currentVer === latestVer && !flags.force) {
419
- out(` \x1b[32m✓\x1b[0m ${s.full_slug.padEnd(38)} v${currentVer.padEnd(8)} [Aktualny]`);
681
+ // Sprawdź wersję lokalną na dysku u agentów lub w .agents/skills
682
+ const localSkill = localSkills.get(slug) || localSkills.get(fullSlug) || localSkills.get(slug.replace(/^@[^/]+\//, ''));
683
+ let localDiskVer = localSkill?.version || null;
684
+
685
+ if (!localDiskVer) {
686
+ for (const agent of agents) {
687
+ const fp = findSkillFileForAgent(agent, s);
688
+ if (fp) {
689
+ const v = readSkillVersion(fp, lock, s);
690
+ if (v) { localDiskVer = v; break; }
691
+ }
692
+ }
693
+ }
694
+
695
+ const effectiveLocalVer = localDiskVer || currentVer;
696
+ const cmpLocalToHub = effectiveLocalVer ? compareSemver(effectiveLocalVer, latestVer) : -1;
697
+
698
+ // 1. Ochrona bezpieczeństwa: Lokalna wersja jest nowsza niż w Hubie (AHEAD)
699
+ if (cmpLocalToHub > 0) {
700
+ const pushDir = localSkill?.dir || (localSkill?.filePath ? path.dirname(localSkill.filePath) : null) || path.join(cwd, '.agents', 'skills', slug);
701
+ aheadSkills.push({
702
+ slug: s.full_slug || s.slug,
703
+ localVersion: effectiveLocalVer,
704
+ hubVersion: latestVer,
705
+ dir: pushDir,
706
+ });
707
+
708
+ if (shouldPush) {
709
+ try {
710
+ out(` \x1b[35m↑ PUSH\x1b[0m ${s.full_slug.padEnd(36)} v${effectiveLocalVer} → Hub (wypychanie lokalnej nowości)...`);
711
+ const pushRes = await skillsHub.pushSkill(pushDir, {
712
+ version: effectiveLocalVer,
713
+ changelog: flags.changelog || `Synchronizacja dwukierunkowa: wypchnięto lokalną wersję v${effectiveLocalVer}`,
714
+ });
715
+ if (pushRes.ok) {
716
+ out(` \x1b[32m✓ PUSHED\x1b[0m ${s.full_slug.padEnd(34)} v${effectiveLocalVer} opublikowano w DeerSkills Hub.`);
717
+ pushedCount += 1;
718
+ lock.skills[slug] = {
719
+ fullSlug: s.full_slug,
720
+ version: effectiveLocalVer,
721
+ installedAt: new Date().toISOString(),
722
+ };
723
+ writeLock(lock, cwd);
724
+ } else {
725
+ out(` \x1b[31m✗ PUSH BŁĄD\x1b[0m ${s.full_slug}: ${pushRes.data?.error || 'Błąd publikacji'}`);
726
+ }
727
+ } catch (pe) {
728
+ out(` \x1b[31m✗ PUSH BŁĄD\x1b[0m ${s.full_slug}: ${pe.message}`);
729
+ }
730
+ } else {
731
+ out(` \x1b[35m★\x1b[0m ${s.full_slug.padEnd(38)} v${effectiveLocalVer.padEnd(8)} [Lokalnie nowszy niż w Hubie (Hub: v${latestVer})]`);
732
+ }
733
+ unchangedCount += 1;
734
+ continue;
735
+ }
736
+
737
+ // 2. Wersje identyczne - aktualny
738
+ if (effectiveLocalVer && cmpLocalToHub === 0 && !flags.force) {
739
+ out(` \x1b[32m✓\x1b[0m ${s.full_slug.padEnd(38)} v${effectiveLocalVer.padEnd(8)} [Aktualny]`);
420
740
  unchangedCount += 1;
421
741
  continue;
422
742
  }
423
743
 
744
+ // 3. Wersja w Hubie nowsza niż lokalna - pobieramy downstream
424
745
  try {
425
746
  await installSingleSkill(s.full_slug || s.slug, latestVer, flags, out, err, { silentSummary: true });
426
- if (currentVer) {
427
- out(` \x1b[33m↑\x1b[0m ${s.full_slug.padEnd(38)} v${latestVer.padEnd(8)} [Zaktualizowano z v${currentVer}]`);
747
+ if (effectiveLocalVer) {
748
+ out(` \x1b[33m↑\x1b[0m ${s.full_slug.padEnd(38)} v${latestVer.padEnd(8)} [Zaktualizowano z v${effectiveLocalVer}]`);
428
749
  updatedCount += 1;
429
750
  } else {
430
751
  out(` \x1b[36m+\x1b[0m ${s.full_slug.padEnd(38)} v${latestVer.padEnd(8)} [Zainstalowano]`);
@@ -436,17 +757,89 @@ async function cmdSkillSync(flags, out, err) {
436
757
  }
437
758
  }
438
759
 
760
+ // 4. Sprawdź lokalne skille, których w ogóle nie ma w Hubie
761
+ const unpushedLocalSkills = [];
762
+ for (const [lSlug, lk] of localSkills.entries()) {
763
+ if (!hubSlugs.has(lSlug)) {
764
+ unpushedLocalSkills.push(lk);
765
+ }
766
+ }
767
+
768
+ if (unpushedLocalSkills.length > 0) {
769
+ for (const ul of unpushedLocalSkills) {
770
+ if (shouldPush) {
771
+ try {
772
+ out(` \x1b[35m↑ PUSH\x1b[0m ${ul.slug.padEnd(36)} v${ul.version} → Hub (nowy skill lokalny)...`);
773
+ const pushRes = await skillsHub.pushSkill(ul.dir, {
774
+ version: ul.version,
775
+ changelog: flags.changelog || `Inicjalna publikacja skilla lokalnego v${ul.version}`,
776
+ });
777
+ if (pushRes.ok) {
778
+ out(` \x1b[32m✓ PUSHED\x1b[0m ${ul.slug.padEnd(34)} v${ul.version} opublikowano w DeerSkills Hub.`);
779
+ pushedCount += 1;
780
+ } else {
781
+ out(` \x1b[31m✗ PUSH BŁĄD\x1b[0m ${ul.slug}: ${pushRes.data?.error || 'Błąd publikacji'}`);
782
+ }
783
+ } catch (pe) {
784
+ out(` \x1b[31m✗ PUSH BŁĄD\x1b[0m ${ul.slug}: ${pe.message}`);
785
+ }
786
+ } else {
787
+ out(` \x1b[90m○\x1b[0m ${ul.slug.padEnd(38)} v${ul.version.padEnd(8)} [Tylko lokalnie (brak w Hubie)]`);
788
+ }
789
+ }
790
+ }
791
+
439
792
  out('\n' + '─'.repeat(72));
440
793
  out(`\x1b[1m✓ Podsumowanie synchronizacji ekosystemu Deer Design:\x1b[0m`);
441
794
  out(` • Nowo zainstalowane: \x1b[36m${installedCount}\x1b[0m`);
442
795
  out(` • Zaktualizowane: \x1b[33m${updatedCount}\x1b[0m`);
443
796
  out(` • Aktualne bez zmian: \x1b[32m${unchangedCount}\x1b[0m`);
797
+ if (pushedCount > 0) {
798
+ out(` • Wypchnięte do Hubu: \x1b[35m${pushedCount}\x1b[0m`);
799
+ }
444
800
  if (failedCount > 0) {
445
801
  out(` • Błędy instalacji: \x1b[31m${failedCount}\x1b[0m`);
446
802
  }
447
803
  out(` • Agenci docelowi: ${agents.map((a) => a.id).join(', ')}`);
448
804
  out(` • Zapisano w rejestrze: ${LOCK_FILE}`);
449
- out('─'.repeat(72) + '\n');
805
+ out('─'.repeat(72));
806
+
807
+ // Alert i komunikat o możliwości wypchnięcia (jeśli nie użyto --push, a wykryto lokalne nowości)
808
+ if (!shouldPush && (aheadSkills.length > 0 || unpushedLocalSkills.length > 0)) {
809
+ out('\n\x1b[35m┌─────────────────────────────────────────────────────────────────────────────┐\x1b[0m');
810
+ out('\x1b[35m│\x1b[0m \x1b[1m💡 WYKRYTO SKILLE Z LOKALNYMI ZMIANAMI GOTOWYMI DO WYPCHNIĘCIA (AHEAD)\x1b[0m \x1b[35m│\x1b[0m');
811
+ out('\x1b[35m├─────────────────────────────────────────────────────────────────────────────┤\x1b[0m');
812
+ out('\x1b[35m│\x1b[0m Domyślny \x1b[36msync\x1b[0m pobiera nowości z Hubu i chroni Twoje lokalne pliki. \x1b[35m│\x1b[0m');
813
+ out('\x1b[35m│\x1b[0m Posiadasz skille nowsze niż w rejestrze firmowym DeerSkills Hub: \x1b[35m│\x1b[0m');
814
+
815
+ const allNotices = [];
816
+ for (const a of aheadSkills) {
817
+ allNotices.push(` • ${a.slug}: lokalnie v${a.localVersion} > w Hubie v${a.hubVersion}`);
818
+ }
819
+ for (const u of unpushedLocalSkills) {
820
+ allNotices.push(` • ${u.slug}: lokalnie v${u.version} (nowy, brak w Hubie)`);
821
+ }
822
+
823
+ const maxToShow = 6;
824
+ for (let i = 0; i < Math.min(allNotices.length, maxToShow); i++) {
825
+ out(`\x1b[35m│\x1b[0m \x1b[33m${allNotices[i].padEnd(73)}\x1b[0m\x1b[35m│\x1b[0m`);
826
+ }
827
+ if (allNotices.length > maxToShow) {
828
+ const moreText = ` ... i jeszcze ${allNotices.length - maxToShow} innych skilli z lokalnymi zmianami`;
829
+ out(`\x1b[35m│\x1b[0m \x1b[90m${moreText.padEnd(73)}\x1b[0m\x1b[35m│\x1b[0m`);
830
+ }
831
+
832
+ out('\x1b[35m├─────────────────────────────────────────────────────────────────────────────┤\x1b[0m');
833
+ out('\x1b[35m│\x1b[0m \x1b[1mAby wypchnąć zmiany do Hubu dla zespołu / firmy:\x1b[0m \x1b[35m│\x1b[0m');
834
+ if (aheadSkills.length > 0) {
835
+ out(`\x1b[35m│\x1b[0m ▶ Pojedynczo: \x1b[36mdeertools skill push ${aheadSkills[0].dir}\x1b[0m`);
836
+ }
837
+ out('\x1b[35m│\x1b[0m ▶ Automatycznie (dwukierunkowo): \x1b[35m│\x1b[0m');
838
+ out('\x1b[35m│\x1b[0m \x1b[32mdeertools sync --push\x1b[0m (lub: \x1b[32mdeertools skill sync --push\x1b[0m) \x1b[35m│\x1b[0m');
839
+ out('\x1b[35m└─────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n');
840
+ } else {
841
+ out('');
842
+ }
450
843
  }
451
844
 
452
845
  async function cmdSkillInstall(target, flags, out, err) {
@@ -985,4 +1378,7 @@ module.exports = {
985
1378
  cmdSkillShare,
986
1379
  checkSkillUpdates,
987
1380
  updateSkills,
1381
+ scanLocalSkills,
1382
+ inspectSkillForAgents,
1383
+ matchSkillQuery,
988
1384
  };