@deerdesign/deertools 1.2.7 → 1.2.9

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.7",
3
+ "version": "1.2.9",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
package/src/commands.js CHANGED
@@ -384,13 +384,19 @@ async function cmdSkill(pos, flags) {
384
384
  if (sub === 'update') {
385
385
  return skillCommands.cmdSkillUpdate(flags, out, err);
386
386
  }
387
+ if (sub === 'versions' || sub === 'history') {
388
+ return skillCommands.cmdSkillVersions(pos[1], flags, out, err);
389
+ }
390
+ if (sub === 'diff' || sub === 'compare') {
391
+ return skillCommands.cmdSkillDiff(pos.slice(1), flags, out, err);
392
+ }
387
393
  if (sub === 'promote') {
388
394
  return skillCommands.cmdSkillPromote(pos[1], flags, out, err);
389
395
  }
390
396
  if (sub === 'install') {
391
397
  return skillCommands.cmdSkillInstall(pos[1], flags, out, err);
392
398
  }
393
- fail('Użycie:\n deertools skill login [token]\n deertools skill bundle <architekci|kosztorysy|logistyka|sprzedaz|it>\n deertools skill list [--tag <tag>]\n deertools skill search <fraza>\n deertools skill install <slug[@version]>\n deertools skill push [katalog] [--version <x.y.z>]\n deertools skill update\n deertools skill promote <slug>', 2);
399
+ fail('Użycie:\n deertools skill login [token]\n deertools skill bundle <architekci|kosztorysy|logistyka|sprzedaz|it>\n deertools skill list [--tag <tag>]\n deertools skill search <fraza>\n deertools skill install <slug[@version]>\n deertools skill versions <slug>\n deertools skill diff <slug> [wersjaA] [wersjaB]\n deertools skill push [katalog] [--bump patch|minor|major] [--changelog "opis"]\n deertools skill update\n deertools skill promote <slug>', 2);
394
400
  }
395
401
 
396
402
 
@@ -531,6 +537,8 @@ Integracje MCP i Agenci AI (DeerSkills Hub):
531
537
  deertools skill list [--tag <tag>] Przegląda skille globalne i użytkownika w DeerSkills Hub.
532
538
  deertools skill search <fraza> Wyszukuje umiejętności po słowach kluczowych.
533
539
  deertools skill install <slug[@wersja]> Instaluje skill w ~/.gemini/antigravity/skills lub .agents.
540
+ deertools skill versions <slug> Historia wydań z changelogami z commitów i datami.
541
+ deertools skill diff <slug> [wersjaA] [wersjaB] Porównanie zmian w plikach paczki i treści SKILL.md.
534
542
  deertools skill push [katalog] [--bump patch|minor] Publikuje nową wersję z automatycznym skanerem sekretów.
535
543
  deertools skill update [slug] Aktualizuje zainstalowane skille do najnowszej wersji.
536
544
  deertools skill promote <slug> Zgłasza nominację skilla do oficjalnego katalogu globalnego.
@@ -120,7 +120,11 @@ async function cmdSkillInstall(target, flags, out, err) {
120
120
  for (const file of versionData.files_manifest) {
121
121
  const targetFile = path.join(baseDir, file.path);
122
122
  fs.mkdirSync(path.dirname(targetFile), { recursive: true });
123
- fs.writeFileSync(targetFile, file.content, 'utf8');
123
+ if (file.encoding === 'base64') {
124
+ fs.writeFileSync(targetFile, Buffer.from(file.content, 'base64'));
125
+ } else {
126
+ fs.writeFileSync(targetFile, file.content, 'utf8');
127
+ }
124
128
  }
125
129
  }
126
130
  } catch (e) {
@@ -145,8 +149,9 @@ async function cmdSkillPush(dir, flags, out, err) {
145
149
  out(`Analizuję folder skilla: ${targetDir}...`);
146
150
 
147
151
  const res = await skillsHub.pushSkill(targetDir, {
148
- version: flags.version || '1.0.0',
149
- changelog: flags.changelog || 'Aktualizacja przez DeerTools CLI',
152
+ version: flags.version || null,
153
+ bump: flags.bump || (flags.major ? 'major' : flags.minor ? 'minor' : 'patch'),
154
+ changelog: flags.changelog || flags.message || flags.m || null,
150
155
  visibility: flags.private ? 'private' : 'shared',
151
156
  });
152
157
 
@@ -154,12 +159,142 @@ async function cmdSkillPush(dir, flags, out, err) {
154
159
  throw new Error(`Nie udało się opublikować skilla (${res.status}): ${res.data?.error || 'Błąd serwera'}`);
155
160
  }
156
161
 
157
- out(`✓ Pomyślnie opublikowano skill w DeerSkills Hub:`);
158
- out(` • Pełny identyfikator: ${res.data.skill.full_slug}`);
159
- out(` • Wersja: ${res.data.version.version}`);
160
- out(` • Widoczność: ${res.data.skill.visibility}`);
161
- out(` • Komenda do instalacji przez innych:`);
162
- out(` deertools skill install ${res.data.skill.full_slug}`);
162
+ const skill = res.data.skill || res.data || {};
163
+ const versionNum = res.data.pushed_version || res.data.version?.version || skill.latest_version;
164
+ const changelog = res.data.pushed_changelog || res.data.version?.changelog || versionNum;
165
+ const manifestCount = res.data.pushed_manifest_count !== undefined
166
+ ? res.data.pushed_manifest_count
167
+ : (res.data.version?.files_manifest?.length || 0);
168
+
169
+ out(`\n✓ Pomyślnie opublikowano skill w DeerSkills Hub:`);
170
+ out(` • Identyfikator: \x1b[1m${skill.full_slug || skill.slug}\x1b[0m`);
171
+ out(` • Wersja: \x1b[32mv${versionNum}\x1b[0m`);
172
+ if (changelog && changelog !== versionNum) {
173
+ out(` • Changelog: \x1b[33m${changelog}\x1b[0m`);
174
+ }
175
+ out(` • Paczka: ${manifestCount + 1} plików (SKILL.md + ${manifestCount} plików w podkatalogach)`);
176
+ out(` • Widoczność: ${skill.visibility || 'shared'}`);
177
+ out(`\nAby zainstalować tę wersję w projekcie:\n \x1b[36mdeertools skill install ${skill.full_slug || skill.slug}@${versionNum}\x1b[0m\n`);
178
+ }
179
+
180
+ async function cmdSkillVersions(slug, flags, out, err) {
181
+ if (!slug) throw new Error('Podaj slug skilla, np. deertools skill versions deer-technical-documentation');
182
+ out(`Pobieram historię wydań dla skilla „${slug}”...`);
183
+ const res = await skillsHub.getSkill(slug);
184
+ if (!res.ok || !res.data?.skill) {
185
+ throw new Error(`Nie znaleziono skilla „${slug}” (${res.status}): ${res.data?.error || 'Błąd'}`);
186
+ }
187
+ const { skill, versions = [] } = res.data;
188
+ out(`\n=== Historia wydań: ${skill.full_slug || skill.slug} (${skill.title || skill.slug}) ===\n`);
189
+ if (versions.length === 0) {
190
+ out('Brak opublikowanych wersji w rejestrze.');
191
+ return;
192
+ }
193
+ const lock = readLock();
194
+ const installedVer = lock.skills?.[skill.slug]?.version;
195
+
196
+ for (let i = 0; i < versions.length; i++) {
197
+ const v = versions[i];
198
+ const isLatest = v.version === skill.latest_version;
199
+ const isInstalled = installedVer && v.version === installedVer;
200
+ const bullet = isLatest ? '\x1b[32m●\x1b[0m' : '○';
201
+ const tag = [
202
+ isLatest ? '\x1b[32m(najnowsza)\x1b[0m' : null,
203
+ isInstalled ? '\x1b[36m[zainstalowana]\x1b[0m' : null,
204
+ ].filter(Boolean).join(' ');
205
+
206
+ const dateStr = v.created_at ? new Date(v.created_at).toLocaleDateString('pl-PL') : '—';
207
+ const fileCount = (v.files_manifest && Array.isArray(v.files_manifest) ? v.files_manifest.length : 0) + 1;
208
+ out(`${bullet} \x1b[1mv${v.version}\x1b[0m ${tag} — ${dateStr}`);
209
+ if (v.changelog) {
210
+ out(` Changelog: \x1b[33m${v.changelog}\x1b[0m`);
211
+ }
212
+ out(` Paczka: ${fileCount} ${fileCount === 1 ? 'plik' : 'plików'} • SHA-256: ${(v.sha256_checksum || '').slice(0, 16)}...`);
213
+ out(` Komenda: deertools skill install ${skill.slug}@${v.version}\n`);
214
+ }
215
+ }
216
+
217
+ async function cmdSkillDiff(args, flags, out, err) {
218
+ const slug = Array.isArray(args) ? args[0] : args;
219
+ if (!slug) throw new Error('Podaj slug skilla, np. deertools skill diff deer-technical-documentation [wersjaA] [wersjaB]');
220
+ const verA = Array.isArray(args) ? args[1] : null;
221
+ const verB = Array.isArray(args) ? args[2] : null;
222
+
223
+ out(`Pobieram dane do porównania dla „${slug}”...`);
224
+ const res = await skillsHub.getSkill(slug);
225
+ if (!res.ok || !res.data?.skill) {
226
+ throw new Error(`Nie znaleziono skilla „${slug}”: ${res.data?.error || 'Błąd'}`);
227
+ }
228
+ const { skill, versions = [] } = res.data;
229
+ if (versions.length < 1) {
230
+ out('Brak wystarczającej liczby wersji do porównania.');
231
+ return;
232
+ }
233
+
234
+ let objA = null;
235
+ let objB = null;
236
+
237
+ if (verA && verB) {
238
+ objA = versions.find((v) => v.version === verA);
239
+ objB = versions.find((v) => v.version === verB);
240
+ if (!objA) throw new Error(`Nie znaleziono wersji ${verA}.`);
241
+ if (!objB) throw new Error(`Nie znaleziono wersji ${verB}.`);
242
+ } else if (verA) {
243
+ objA = versions.find((v) => v.version === verA);
244
+ objB = versions.find((v) => v.version === skill.latest_version) || versions[0];
245
+ if (!objA) throw new Error(`Nie znaleziono wersji ${verA}.`);
246
+ } else {
247
+ const lock = readLock();
248
+ const installed = lock.skills?.[skill.slug]?.version;
249
+ if (installed && installed !== skill.latest_version) {
250
+ objA = versions.find((v) => v.version === installed);
251
+ objB = versions.find((v) => v.version === skill.latest_version);
252
+ } else if (versions.length >= 2) {
253
+ objA = versions[1];
254
+ objB = versions[0];
255
+ } else {
256
+ out(`Dostępna jest tylko jedna wersja (v${versions[0].version}). Brak wcześniejszych wydań do porównania.`);
257
+ return;
258
+ }
259
+ }
260
+
261
+ out(`\n=== Porównanie: ${skill.slug} v${objA.version} → v${objB.version} ===\n`);
262
+ if (objB.changelog) {
263
+ out(`Opis zmian (v${objB.version}): \x1b[33m${objB.changelog}\x1b[0m\n`);
264
+ }
265
+
266
+ // 1. Zmiany w strukturze plików paczki
267
+ const manA = objA.files_manifest || [];
268
+ const manB = objB.files_manifest || [];
269
+ const comp = skillsHub.compareManifests(manA, manB);
270
+
271
+ out(`Pliki w paczce:`);
272
+ if (comp.added.length === 0 && comp.removed.length === 0 && comp.modified.length === 0) {
273
+ out(` (brak zmian w strukturze plików paczki)`);
274
+ } else {
275
+ for (const f of comp.added) out(` \x1b[32m+ ${f.path}\x1b[0m (nowy plik, ${Math.round((f.size || 0) / 1024 * 10) / 10 || '<1'} KB)`);
276
+ for (const f of comp.removed) out(` \x1b[31m- ${f.path}\x1b[0m (usunięty plik)`);
277
+ for (const f of comp.modified) out(` \x1b[33m~ ${f.path}\x1b[0m (zmodyfikowany plik)`);
278
+ }
279
+ out('');
280
+
281
+ // 2. Diff tekstu SKILL.md
282
+ out(`Różnice w SKILL.md (v${objA.version} → v${objB.version}):`);
283
+ const diff = skillsHub.diffLines(objA.skill_md_content, objB.skill_md_content);
284
+ let hasDiff = false;
285
+ for (const p of diff) {
286
+ if (p.type === 'added') {
287
+ out(`\x1b[32m+ ${p.line}\x1b[0m`);
288
+ hasDiff = true;
289
+ } else if (p.type === 'removed') {
290
+ out(`\x1b[31m- ${p.line}\x1b[0m`);
291
+ hasDiff = true;
292
+ }
293
+ }
294
+ if (!hasDiff) {
295
+ out(` (brak zmian w pliku SKILL.md)`);
296
+ }
297
+ out('');
163
298
  }
164
299
 
165
300
  async function cmdSkillUpdate(flags, out, err) {
@@ -223,4 +358,6 @@ module.exports = {
223
358
  cmdSkillPush,
224
359
  cmdSkillUpdate,
225
360
  cmdSkillPromote,
361
+ cmdSkillVersions,
362
+ cmdSkillDiff,
226
363
  };
package/src/skills-hub.js CHANGED
@@ -91,15 +91,127 @@ async function getSkill(slug, version = null) {
91
91
  return hubRequest(`/skills/${encodeURIComponent(slug)}${qs}`);
92
92
  }
93
93
 
94
- async function pushSkill(skillDir, { version = '1.0.0', changelog = 'Publikacja przez DeerTools CLI', visibility = 'shared' } = {}) {
95
- const mdPath = fs.existsSync(skillDir) && fs.statSync(skillDir).isFile()
96
- ? skillDir
97
- : path.join(skillDir, 'SKILL.md');
94
+ function bumpVersion(currentVersion = '1.0.0', type = 'patch') {
95
+ const parts = String(currentVersion || '1.0.0').replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
96
+ let [major = 1, minor = 0, patch = 0] = parts;
97
+ if (type === 'major') {
98
+ major += 1;
99
+ minor = 0;
100
+ patch = 0;
101
+ } else if (type === 'minor') {
102
+ minor += 1;
103
+ patch = 0;
104
+ } else {
105
+ patch += 1;
106
+ }
107
+ return `${major}.${minor}.${patch}`;
108
+ }
109
+
110
+ function getGitCommitMessage(dir) {
111
+ try {
112
+ const { execSync } = require('child_process');
113
+ const msg = execSync('git log -1 --pretty=%B', { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 })
114
+ .toString('utf8')
115
+ .trim();
116
+ return msg || null;
117
+ } catch (_) {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ function isTextBuffer(buf) {
123
+ const max = Math.min(buf.length, 512);
124
+ for (let i = 0; i < max; i++) {
125
+ if (buf[i] === 0) return false;
126
+ }
127
+ return true;
128
+ }
129
+
130
+ function gatherSkillBundle(skillDir) {
131
+ const isFile = fs.existsSync(skillDir) && fs.statSync(skillDir).isFile();
132
+ const mdPath = isFile ? skillDir : path.join(skillDir, 'SKILL.md');
98
133
 
99
134
  if (!fs.existsSync(mdPath)) {
100
135
  throw new Error(`Nie znaleziono pliku SKILL.md w ścieżce: ${skillDir}`);
101
136
  }
102
137
 
138
+ const baseDir = isFile ? path.dirname(skillDir) : skillDir;
139
+ const manifest = [];
140
+ const ignored = new Set(['.git', 'node_modules', '.DS_Store', '__pycache__', '.pytest_cache', '.turbo']);
141
+
142
+ function walk(currentDir, relPrefix = '') {
143
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
144
+ for (const ent of entries) {
145
+ if (ignored.has(ent.name) || ent.name.startsWith('.')) continue;
146
+ const fullPath = path.join(currentDir, ent.name);
147
+ const relPath = relPrefix ? `${relPrefix}/${ent.name}` : ent.name;
148
+
149
+ if (ent.isDirectory()) {
150
+ walk(fullPath, relPath);
151
+ } else if (ent.isFile() && relPath !== 'SKILL.md') {
152
+ const buf = fs.readFileSync(fullPath);
153
+ const isText = isTextBuffer(buf);
154
+ const content = isText ? buf.toString('utf8') : buf.toString('base64');
155
+ const encoding = isText ? 'utf8' : 'base64';
156
+ const sha = crypto.createHash('sha256').update(buf).digest('hex');
157
+
158
+ manifest.push({
159
+ path: relPath,
160
+ content,
161
+ encoding,
162
+ size: buf.length,
163
+ sha256: sha,
164
+ });
165
+ }
166
+ }
167
+ }
168
+
169
+ walk(baseDir);
170
+ return { mdPath, baseDir, manifest };
171
+ }
172
+
173
+ function diffLines(textA, textB) {
174
+ const linesA = String(textA || '').split('\n');
175
+ const linesB = String(textB || '').split('\n');
176
+ const diff = [];
177
+ let i = 0, j = 0;
178
+ while (i < linesA.length || j < linesB.length) {
179
+ if (i < linesA.length && j < linesB.length && linesA[i] === linesB[j]) {
180
+ diff.push({ type: 'same', line: linesA[i] });
181
+ i++; j++;
182
+ } else if (j < linesB.length && (!linesA.includes(linesB[j]) || linesB.indexOf(linesA[i]) > j)) {
183
+ diff.push({ type: 'added', line: linesB[j] });
184
+ j++;
185
+ } else if (i < linesA.length) {
186
+ diff.push({ type: 'removed', line: linesA[i] });
187
+ i++;
188
+ } else {
189
+ diff.push({ type: 'added', line: linesB[j] });
190
+ j++;
191
+ }
192
+ }
193
+ return diff;
194
+ }
195
+
196
+ function compareManifests(manifestA = [], manifestB = []) {
197
+ const mapA = new Map((manifestA || []).map((f) => [f.path, f]));
198
+ const mapB = new Map((manifestB || []).map((f) => [f.path, f]));
199
+ const added = [];
200
+ const removed = [];
201
+ const modified = [];
202
+
203
+ for (const [p, f] of mapB) {
204
+ if (!mapA.has(p)) added.push(f);
205
+ else if (mapA.get(p).sha256 !== f.sha256) modified.push({ path: p, old: mapA.get(p), new: f });
206
+ }
207
+ for (const [p, f] of mapA) {
208
+ if (!mapB.has(p)) removed.push(f);
209
+ }
210
+ return { added, removed, modified };
211
+ }
212
+
213
+ async function pushSkill(skillDir, { version = null, bump = 'patch', changelog = null, visibility = 'shared' } = {}) {
214
+ const { mdPath, baseDir, manifest } = gatherSkillBundle(skillDir);
103
215
  const skillContent = fs.readFileSync(mdPath, 'utf8');
104
216
 
105
217
  // 1. Skanowanie sekretów
@@ -109,8 +221,18 @@ async function pushSkill(skillDir, { version = '1.0.0', changelog = 'Publikacja
109
221
  throw new Error(`BLOKADA BEZPIECZEŃSTWA: Wykryto potencjalne klucze API w treści SKILL.md [${list}]. Usuń sekrety przed publikacją!`);
110
222
  }
111
223
 
224
+ for (const file of manifest) {
225
+ if (file.encoding === 'utf8') {
226
+ const fScan = scanForSecrets(file.content);
227
+ if (!fScan.safe) {
228
+ const list = fScan.matches.map((m) => `${m.name} (${m.count})`).join(', ');
229
+ throw new Error(`BLOKADA BEZPIECZEŃSTWA: Wykryto potencjalne klucze w ${file.path} [${list}]. Usuń sekrety przed publikacją!`);
230
+ }
231
+ }
232
+ }
233
+
112
234
  // 2. Parsowanie frontmatter
113
- let slug = path.basename(path.dirname(mdPath));
235
+ let slug = path.basename(baseDir);
114
236
  let title = slug;
115
237
  let description = '';
116
238
  if (skillContent.startsWith('---')) {
@@ -126,42 +248,44 @@ async function pushSkill(skillDir, { version = '1.0.0', changelog = 'Publikacja
126
248
  }
127
249
  }
128
250
 
129
- // 3. Odczyt dodatkowych plików (references/, scripts/, assets/)
130
- const dir = path.dirname(mdPath);
131
- const filesManifest = [];
132
- const allowedDirs = ['references', 'scripts', 'assets'];
133
-
134
- for (const sub of allowedDirs) {
135
- const subPath = path.join(dir, sub);
136
- if (fs.existsSync(subPath) && fs.statSync(subPath).isDirectory()) {
137
- const files = fs.readdirSync(subPath);
138
- for (const f of files) {
139
- const fullF = path.join(subPath, f);
140
- if (fs.statSync(fullF).isFile() && !f.startsWith('.')) {
141
- try {
142
- const rel = path.join(sub, f);
143
- const content = fs.readFileSync(fullF, 'utf8');
144
- filesManifest.push({ path: rel, content });
145
- } catch (_) {
146
- // binarne pomijamy w manifeście tekstowym
147
- }
148
- }
149
- }
251
+ // 3. Automatyczne ustalenie wersji, jeśli nie podano
252
+ let targetVersion = version;
253
+ if (!targetVersion) {
254
+ try {
255
+ const existing = await getSkill(slug);
256
+ const latestVer = existing?.data?.skill?.latest_version;
257
+ targetVersion = latestVer ? bumpVersion(latestVer, bump) : '1.0.0';
258
+ } catch (_) {
259
+ targetVersion = '1.0.0';
150
260
  }
151
261
  }
152
262
 
263
+ // 4. Automatyczny opis z ostatniego commita git, jeśli nie podano changelog
264
+ let finalChangelog = changelog;
265
+ if (!finalChangelog) {
266
+ const gitMsg = getGitCommitMessage(baseDir);
267
+ finalChangelog = gitMsg || 'Aktualizacja paczki przez DeerTools CLI';
268
+ }
269
+
153
270
  const body = {
154
271
  slug,
155
272
  title,
156
273
  summary: description,
157
- version,
158
- changelog,
274
+ version: targetVersion,
275
+ changelog: finalChangelog,
159
276
  visibility,
277
+ skill_md: skillContent,
160
278
  skill_md_content: skillContent,
161
- files_manifest: filesManifest,
279
+ files_manifest: manifest,
162
280
  };
163
281
 
164
- return hubRequest('/skills/push', { method: 'POST', body });
282
+ const res = await hubRequest('/skills/push', { method: 'POST', body });
283
+ if (res.ok && res.data) {
284
+ res.data.pushed_version = targetVersion;
285
+ res.data.pushed_manifest_count = manifest.length;
286
+ res.data.pushed_changelog = finalChangelog;
287
+ }
288
+ return res;
165
289
  }
166
290
 
167
291
  async function listMcpServers() {
@@ -194,6 +318,11 @@ module.exports = {
194
318
  listSkills,
195
319
  getSkill,
196
320
  pushSkill,
321
+ bumpVersion,
322
+ gatherSkillBundle,
323
+ diffLines,
324
+ compareManifests,
325
+ getGitCommitMessage,
197
326
  listMcpServers,
198
327
  getProducts,
199
328
  getDocs,