skillex 0.2.4 → 0.3.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/dist/install.js CHANGED
@@ -36,9 +36,6 @@ export async function initProject(options = {}) {
36
36
  lockfile.sources = [toLockfileSource(source)];
37
37
  }
38
38
  lockfile.settings.autoSync = options.autoSync ?? lockfile.settings.autoSync;
39
- if (lockfile.settings.autoSync && !lockfile.adapters.active) {
40
- throw new InstallError("Auto-sync requires an active adapter. Use --adapter <id> or run in a detectable workspace.", "AUTO_SYNC_REQUIRES_ADAPTER");
41
- }
42
39
  lockfile.updatedAt = now();
43
40
  await writeJson(statePaths.lockfilePath, lockfile);
44
41
  // Create .gitignore for the local state directory on first init.
@@ -129,7 +126,8 @@ export async function installSkills(requestedSkillIds, options = {}) {
129
126
  const autoSync = await maybeAutoSync(withAgentSkillsDir({
130
127
  cwd,
131
128
  scope: options.scope,
132
- adapter: lockfile.adapters.active,
129
+ adapters: lockfile.adapters,
130
+ adapterOverride: options.adapter,
133
131
  enabled: lockfile.settings.autoSync,
134
132
  now,
135
133
  changed: installedSkills.length > 0,
@@ -217,7 +215,8 @@ export async function updateInstalledSkills(requestedSkillIds, options = {}) {
217
215
  const autoSync = await maybeAutoSync(withAgentSkillsDir({
218
216
  cwd,
219
217
  scope: options.scope,
220
- adapter: lockfile.adapters.active,
218
+ adapters: lockfile.adapters,
219
+ adapterOverride: options.adapter,
221
220
  enabled: lockfile.settings.autoSync,
222
221
  now,
223
222
  changed: updatedSkills.length > 0,
@@ -272,10 +271,13 @@ export async function removeSkills(requestedSkillIds, options = {}) {
272
271
  }
273
272
  lockfile.updatedAt = now();
274
273
  await writeJson(statePaths.lockfilePath, lockfile);
275
- const autoSync = await maybeAutoSync(withAgentSkillsDir({
274
+ const autoSync = await maybeSyncAfterRemove(withAgentSkillsDir({
276
275
  cwd,
277
276
  scope: options.scope,
278
- adapter: lockfile.adapters.active,
277
+ adapters: lockfile.adapters,
278
+ adapterOverride: options.adapter,
279
+ syncHistory: lockfile.syncHistory,
280
+ legacySync: lockfile.sync,
279
281
  enabled: lockfile.settings.autoSync,
280
282
  now,
281
283
  changed: removedSkills.length > 0,
@@ -312,55 +314,93 @@ export async function syncInstalledSkills(options = {}) {
312
314
  }
313
315
  const defaultSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
314
316
  const lockfile = normalizeLockfile(existing, defaultSource, now);
315
- const adapterId = options.adapter || lockfile.adapters.active;
316
- if (!adapterId) {
317
+ const adapterIds = resolveSyncAdapterIds(lockfile.adapters, options.adapter);
318
+ if (adapterIds.length === 0) {
317
319
  throw new InstallError("No active adapter configured. Run: skillex init --adapter <id> or use --adapter.", "ACTIVE_ADAPTER_MISSING");
318
320
  }
319
321
  const skills = await loadInstalledSkillDocuments({
320
322
  cwd,
321
323
  lockfile,
322
324
  });
323
- const syncResult = await syncAdapterFiles({
324
- cwd,
325
- scope: statePaths.scope,
326
- adapterId,
327
- statePaths,
328
- skills,
329
- previousSkillIds: lockfile.sync?.skillIds || [],
330
- ...(options.mode ? { mode: options.mode } : {}),
331
- ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
332
- });
325
+ const syncResults = [];
326
+ const diffParts = [];
327
+ for (const adapterId of adapterIds) {
328
+ const syncResult = await syncAdapterFiles({
329
+ cwd,
330
+ scope: statePaths.scope,
331
+ adapterId,
332
+ statePaths,
333
+ skills,
334
+ previousSkillIds: lockfile.syncHistory[adapterId]?.skillIds || lockfile.sync?.skillIds || [],
335
+ ...(options.mode ? { mode: options.mode } : {}),
336
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
337
+ });
338
+ syncResults.push(syncResult);
339
+ if (syncResult.diff.trim()) {
340
+ diffParts.push(syncResult.diff.trimEnd());
341
+ }
342
+ }
343
+ const primarySync = syncResults[0];
344
+ if (!primarySync) {
345
+ throw new InstallError("No adapter configured for synchronization. Use --adapter <id> or work in a detectable workspace.", "ACTIVE_ADAPTER_MISSING");
346
+ }
333
347
  if (options.dryRun) {
334
348
  return {
335
349
  statePaths,
336
350
  sync: {
337
- adapter: syncResult.adapter,
338
- targetPath: syncResult.targetPath,
351
+ adapter: primarySync.adapter,
352
+ targetPath: primarySync.targetPath,
339
353
  },
354
+ syncs: syncResults.map((result) => ({
355
+ adapter: result.adapter,
356
+ targetPath: result.targetPath,
357
+ syncMode: result.syncMode,
358
+ changed: result.changed,
359
+ })),
340
360
  skillCount: skills.length,
341
- changed: syncResult.changed,
342
- diff: syncResult.diff,
361
+ changed: syncResults.some((result) => result.changed),
362
+ diff: diffParts.length > 0 ? `${diffParts.join("\n\n")}\n` : "",
343
363
  dryRun: true,
344
- syncMode: syncResult.syncMode,
364
+ syncMode: primarySync.syncMode,
345
365
  };
346
366
  }
347
- lockfile.sync = {
348
- adapter: syncResult.adapter,
349
- targetPath: syncResult.targetPath,
367
+ const primaryMetadata = {
368
+ adapter: primarySync.adapter,
369
+ targetPath: primarySync.targetPath,
350
370
  syncedAt: now(),
351
371
  skillIds: skills.map((skill) => skill.id),
352
372
  };
353
- lockfile.syncMode = syncResult.syncMode;
373
+ const nextSyncHistory = {
374
+ ...lockfile.syncHistory,
375
+ [primarySync.adapter]: primaryMetadata,
376
+ };
377
+ for (const syncResult of syncResults.slice(1)) {
378
+ nextSyncHistory[syncResult.adapter] = {
379
+ adapter: syncResult.adapter,
380
+ targetPath: syncResult.targetPath,
381
+ syncedAt: now(),
382
+ skillIds: skills.map((skill) => skill.id),
383
+ };
384
+ }
385
+ lockfile.sync = primaryMetadata;
386
+ lockfile.syncHistory = nextSyncHistory;
387
+ lockfile.syncMode = primarySync.syncMode;
354
388
  lockfile.updatedAt = now();
355
389
  await writeJson(statePaths.lockfilePath, lockfile);
356
390
  return {
357
391
  statePaths,
358
- sync: lockfile.sync,
392
+ sync: primaryMetadata,
393
+ syncs: syncResults.map((result) => ({
394
+ adapter: result.adapter,
395
+ targetPath: result.targetPath,
396
+ syncMode: result.syncMode,
397
+ changed: result.changed,
398
+ })),
359
399
  skillCount: skills.length,
360
- changed: syncResult.changed,
361
- diff: syncResult.diff,
400
+ changed: syncResults.some((result) => result.changed),
401
+ diff: diffParts.length > 0 ? `${diffParts.join("\n\n")}\n` : "",
362
402
  dryRun: false,
363
- syncMode: syncResult.syncMode,
403
+ syncMode: primarySync.syncMode,
364
404
  };
365
405
  }
366
406
  catch (error) {
@@ -541,9 +581,10 @@ function createBaseLockfile(source, now) {
541
581
  detected: [],
542
582
  },
543
583
  settings: {
544
- autoSync: false,
584
+ autoSync: true,
545
585
  },
546
586
  sync: null,
587
+ syncHistory: {},
547
588
  syncMode: null,
548
589
  installed: {},
549
590
  };
@@ -666,13 +707,35 @@ function normalizeLockfile(existing, source, now) {
666
707
  detected: [...new Set(detectedAdapters.filter(Boolean))],
667
708
  },
668
709
  settings: {
669
- autoSync: Boolean(existing.settings?.autoSync),
710
+ autoSync: existing.settings?.autoSync ?? true,
670
711
  },
671
712
  sync: existing.sync || null,
713
+ syncHistory: normalizeSyncHistory(existing),
672
714
  syncMode: existing.syncMode || null,
673
715
  installed: existing.installed || {},
674
716
  };
675
717
  }
718
+ function normalizeSyncHistory(existing) {
719
+ const history = {};
720
+ const candidate = existing && "syncHistory" in existing && existing.syncHistory && typeof existing.syncHistory === "object"
721
+ ? existing.syncHistory
722
+ : null;
723
+ if (candidate) {
724
+ for (const [adapterId, metadata] of Object.entries(candidate)) {
725
+ if (!metadata || typeof metadata !== "object") {
726
+ continue;
727
+ }
728
+ if (!("adapter" in metadata) || !("targetPath" in metadata) || !("syncedAt" in metadata)) {
729
+ continue;
730
+ }
731
+ history[adapterId] = metadata;
732
+ }
733
+ }
734
+ if (existing?.sync?.adapter && !history[existing.sync.adapter]) {
735
+ history[existing.sync.adapter] = existing.sync;
736
+ }
737
+ return history;
738
+ }
676
739
  /** Repos that are known placeholder values written by older versions and must be ignored. */
677
740
  const PLACEHOLDER_REPOS = new Set(["owner/repo"]);
678
741
  function getLockfileSources(existing, fallbackSource) {
@@ -835,15 +898,65 @@ async function maybeAutoSync(options) {
835
898
  if (!options.enabled || !options.changed) {
836
899
  return null;
837
900
  }
901
+ if (resolveSyncAdapterIds(options.adapters, options.adapterOverride).length === 0) {
902
+ return null;
903
+ }
838
904
  return syncInstalledSkills({
839
905
  cwd: options.cwd,
840
906
  scope: options.scope || DEFAULT_INSTALL_SCOPE,
841
907
  ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
842
- ...(options.adapter ? { adapter: options.adapter } : {}),
908
+ ...(options.adapterOverride ? { adapter: options.adapterOverride } : {}),
843
909
  ...(options.mode ? { mode: options.mode } : {}),
844
910
  now: options.now,
845
911
  });
846
912
  }
913
+ async function maybeSyncAfterRemove(options) {
914
+ if (!options.changed) {
915
+ return null;
916
+ }
917
+ const adapters = new Set();
918
+ for (const adapterId of Object.keys(options.syncHistory || {})) {
919
+ adapters.add(adapterId);
920
+ }
921
+ if (options.legacySync?.adapter) {
922
+ adapters.add(options.legacySync.adapter);
923
+ }
924
+ if (options.adapterOverride) {
925
+ adapters.add(options.adapterOverride);
926
+ }
927
+ else if (options.enabled) {
928
+ for (const adapterId of resolveSyncAdapterIds(options.adapters)) {
929
+ adapters.add(adapterId);
930
+ }
931
+ }
932
+ let result = null;
933
+ for (const adapterId of adapters) {
934
+ result = await syncInstalledSkills({
935
+ cwd: options.cwd,
936
+ scope: options.scope || DEFAULT_INSTALL_SCOPE,
937
+ ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
938
+ adapter: adapterId,
939
+ ...(options.mode ? { mode: options.mode } : {}),
940
+ now: options.now,
941
+ });
942
+ }
943
+ return result;
944
+ }
945
+ function resolveSyncAdapterIds(adapters, adapterOverride) {
946
+ if (adapterOverride) {
947
+ return [adapterOverride];
948
+ }
949
+ const adapterIds = [];
950
+ if (adapters.active) {
951
+ adapterIds.push(adapters.active);
952
+ }
953
+ for (const adapterId of adapters.detected || []) {
954
+ if (!adapterIds.includes(adapterId)) {
955
+ adapterIds.push(adapterId);
956
+ }
957
+ }
958
+ return adapterIds;
959
+ }
847
960
  function toCatalogSourceInput(options, overrides = {}) {
848
961
  const input = {};
849
962
  if (options.owner) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Renders a safe HTML representation for common Markdown constructs used by skills.
3
+ *
4
+ * @param markdown - Raw markdown source.
5
+ * @returns Sanitized HTML string.
6
+ */
7
+ export declare function renderMarkdownToHtml(markdown: string): string;
@@ -0,0 +1,193 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+ function renderInlineMarkdown(value) {
10
+ let html = escapeHtml(value);
11
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_match, label, url) => {
12
+ const safeLabel = escapeHtml(label);
13
+ const safeUrl = escapeHtml(url);
14
+ return `<a href="${safeUrl}" target="_blank" rel="noreferrer noopener">${safeLabel}</a>`;
15
+ });
16
+ html = html.replace(/`([^`]+)`/g, (_match, code) => `<code>${escapeHtml(code)}</code>`);
17
+ html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
18
+ html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
19
+ return html;
20
+ }
21
+ function flushParagraph(paragraph, html) {
22
+ if (paragraph.length === 0) {
23
+ return;
24
+ }
25
+ html.push(`<p>${paragraph.map((line) => renderInlineMarkdown(line)).join("<br>")}</p>`);
26
+ paragraph.length = 0;
27
+ }
28
+ function closeList(currentList, html) {
29
+ if (currentList.type) {
30
+ html.push(`</${currentList.type}>`);
31
+ currentList.type = null;
32
+ }
33
+ }
34
+ function isTableSeparator(line) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) {
37
+ return false;
38
+ }
39
+ return trimmed
40
+ .slice(1, -1)
41
+ .split("|")
42
+ .every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
43
+ }
44
+ function splitTableRow(line) {
45
+ return line
46
+ .trim()
47
+ .slice(1, -1)
48
+ .split("|")
49
+ .map((cell) => renderInlineMarkdown(cell.trim()));
50
+ }
51
+ function consumeTable(lines, startIndex) {
52
+ const headerLine = lines[startIndex]?.trim();
53
+ const separatorLine = lines[startIndex + 1]?.trim();
54
+ if (!headerLine || !separatorLine || !headerLine.startsWith("|") || !headerLine.endsWith("|") || !isTableSeparator(separatorLine)) {
55
+ return null;
56
+ }
57
+ const headers = splitTableRow(headerLine);
58
+ const rows = [];
59
+ let index = startIndex + 2;
60
+ while (index < lines.length) {
61
+ const candidate = lines[index]?.trim();
62
+ if (!candidate || !candidate.startsWith("|") || !candidate.endsWith("|")) {
63
+ break;
64
+ }
65
+ rows.push(splitTableRow(candidate));
66
+ index += 1;
67
+ }
68
+ const html = [
69
+ "<div class=\"markdown-table-wrap\">",
70
+ "<table>",
71
+ "<thead>",
72
+ "<tr>",
73
+ ...headers.map((header) => `<th>${header}</th>`),
74
+ "</tr>",
75
+ "</thead>",
76
+ "<tbody>",
77
+ ...rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`),
78
+ "</tbody>",
79
+ "</table>",
80
+ "</div>",
81
+ ].join("");
82
+ return {
83
+ html,
84
+ nextIndex: index - 1,
85
+ };
86
+ }
87
+ /**
88
+ * Renders a safe HTML representation for common Markdown constructs used by skills.
89
+ *
90
+ * @param markdown - Raw markdown source.
91
+ * @returns Sanitized HTML string.
92
+ */
93
+ export function renderMarkdownToHtml(markdown) {
94
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
95
+ const html = [];
96
+ const paragraph = [];
97
+ const currentList = { type: null };
98
+ let inCodeFence = false;
99
+ let codeFenceLanguage = "";
100
+ const codeFenceLines = [];
101
+ for (let index = 0; index < lines.length; index += 1) {
102
+ const line = lines[index] ?? "";
103
+ const trimmed = line.trim();
104
+ if (trimmed.startsWith("```")) {
105
+ flushParagraph(paragraph, html);
106
+ closeList(currentList, html);
107
+ if (inCodeFence) {
108
+ const languageClass = codeFenceLanguage ? ` class="language-${escapeHtml(codeFenceLanguage)}"` : "";
109
+ html.push(`<pre><code${languageClass}>${escapeHtml(codeFenceLines.join("\n"))}</code></pre>`);
110
+ codeFenceLines.length = 0;
111
+ codeFenceLanguage = "";
112
+ inCodeFence = false;
113
+ }
114
+ else {
115
+ inCodeFence = true;
116
+ codeFenceLanguage = trimmed.slice(3).trim();
117
+ }
118
+ continue;
119
+ }
120
+ if (inCodeFence) {
121
+ codeFenceLines.push(line);
122
+ continue;
123
+ }
124
+ const table = consumeTable(lines, index);
125
+ if (table) {
126
+ flushParagraph(paragraph, html);
127
+ closeList(currentList, html);
128
+ html.push(table.html);
129
+ index = table.nextIndex;
130
+ continue;
131
+ }
132
+ if (!trimmed) {
133
+ flushParagraph(paragraph, html);
134
+ closeList(currentList, html);
135
+ continue;
136
+ }
137
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
138
+ if (headingMatch) {
139
+ flushParagraph(paragraph, html);
140
+ closeList(currentList, html);
141
+ const level = headingMatch[1]?.length ?? 1;
142
+ html.push(`<h${level}>${renderInlineMarkdown(headingMatch[2] ?? "")}</h${level}>`);
143
+ continue;
144
+ }
145
+ const unorderedMatch = trimmed.match(/^[-*+]\s+(.+)$/);
146
+ if (unorderedMatch) {
147
+ flushParagraph(paragraph, html);
148
+ if (currentList.type !== "ul") {
149
+ closeList(currentList, html);
150
+ currentList.type = "ul";
151
+ html.push("<ul>");
152
+ }
153
+ html.push(`<li>${renderInlineMarkdown(unorderedMatch[1] ?? "")}</li>`);
154
+ continue;
155
+ }
156
+ const orderedMatch = trimmed.match(/^\d+\.\s+(.+)$/);
157
+ if (orderedMatch) {
158
+ flushParagraph(paragraph, html);
159
+ if (currentList.type !== "ol") {
160
+ closeList(currentList, html);
161
+ currentList.type = "ol";
162
+ html.push("<ol>");
163
+ }
164
+ html.push(`<li>${renderInlineMarkdown(orderedMatch[1] ?? "")}</li>`);
165
+ continue;
166
+ }
167
+ const quoteMatch = trimmed.match(/^>\s?(.*)$/);
168
+ if (quoteMatch) {
169
+ flushParagraph(paragraph, html);
170
+ closeList(currentList, html);
171
+ const quoteLines = [renderInlineMarkdown(quoteMatch[1] ?? "")];
172
+ while (index + 1 < lines.length) {
173
+ const next = lines[index + 1]?.trim() ?? "";
174
+ const nextQuote = next.match(/^>\s?(.*)$/);
175
+ if (!nextQuote) {
176
+ break;
177
+ }
178
+ quoteLines.push(renderInlineMarkdown(nextQuote[1] ?? ""));
179
+ index += 1;
180
+ }
181
+ html.push(`<blockquote><p>${quoteLines.join("<br>")}</p></blockquote>`);
182
+ continue;
183
+ }
184
+ paragraph.push(trimmed);
185
+ }
186
+ if (inCodeFence) {
187
+ const languageClass = codeFenceLanguage ? ` class="language-${escapeHtml(codeFenceLanguage)}"` : "";
188
+ html.push(`<pre><code${languageClass}>${escapeHtml(codeFenceLines.join("\n"))}</code></pre>`);
189
+ }
190
+ flushParagraph(paragraph, html);
191
+ closeList(currentList, html);
192
+ return html.join("\n");
193
+ }
package/dist/sync.js CHANGED
@@ -51,7 +51,16 @@ export async function syncAdapterFiles(options) {
51
51
  try {
52
52
  const prepared = await prepareSyncAdapterFiles(options);
53
53
  if (!options.dryRun) {
54
- if (prepared.directoryEntries) {
54
+ if (prepared.removeTarget) {
55
+ if (prepared.generatedSourcePath) {
56
+ await removePath(prepared.generatedSourcePath);
57
+ }
58
+ await removePath(prepared.absoluteTargetPath);
59
+ for (const cleanupPath of prepared.cleanupPaths) {
60
+ await removePath(cleanupPath);
61
+ }
62
+ }
63
+ else if (prepared.directoryEntries) {
55
64
  await ensureDir(prepared.absoluteTargetPath);
56
65
  const createLink = options.linkFactory || createSymlink;
57
66
  let finalMode = prepared.syncMode;
@@ -144,8 +153,28 @@ export async function prepareSyncAdapterFiles(options) {
144
153
  const autoInjectBlock = buildAutoInjectBlock(options.skills);
145
154
  if (adapter.syncMode === "managed-block") {
146
155
  const existing = (await readText(absoluteTargetPath, "")) || "";
147
- const nextManaged = upsertManagedBlock(existing, wrapManagedBlock(MANAGED_START, MANAGED_END, body));
148
- const nextContent = upsertAutoInjectBlock(nextManaged, autoInjectBlock);
156
+ const nextManaged = options.skills.length === 0
157
+ ? upsertManagedBlock(existing, null)
158
+ : upsertManagedBlock(existing, wrapManagedBlock(MANAGED_START, MANAGED_END, body));
159
+ const nextContent = upsertAutoInjectBlock(nextManaged, options.skills.length === 0 ? null : autoInjectBlock);
160
+ if (nextContent === "") {
161
+ return {
162
+ adapter: adapter.id,
163
+ absoluteTargetPath,
164
+ targetPath,
165
+ cleanupPaths,
166
+ removeTarget: true,
167
+ changed: Boolean(normalizeComparableText(existing)) || cleanupPaths.length > 0,
168
+ currentContent: existing,
169
+ nextContent: "",
170
+ diff: createManagedBlockRemovalDiff({
171
+ targetPath,
172
+ currentContent: existing,
173
+ cleanupPaths: cleanupPaths.map((cleanupPath) => toDisplayPath(options.cwd, cleanupPath, options.statePaths.scope)),
174
+ }),
175
+ syncMode: "copy",
176
+ };
177
+ }
149
178
  return {
150
179
  adapter: adapter.id,
151
180
  absoluteTargetPath,
@@ -160,6 +189,35 @@ export async function prepareSyncAdapterFiles(options) {
160
189
  }
161
190
  const nextContent = buildManagedFileContent(adapter.id, body, autoInjectBlock);
162
191
  const requestedMode = options.mode || "symlink";
192
+ const generatedSourcePath = path.join(options.statePaths.generatedDirPath, adapter.id, path.basename(adapter.syncTarget));
193
+ if (options.skills.length === 0) {
194
+ const currentDescriptor = await describeTarget(absoluteTargetPath);
195
+ const currentVisibleContent = (await readText(absoluteTargetPath, "")) || "";
196
+ const generatedExists = await pathExists(generatedSourcePath);
197
+ return {
198
+ adapter: adapter.id,
199
+ absoluteTargetPath,
200
+ targetPath,
201
+ cleanupPaths,
202
+ removeTarget: true,
203
+ changed: Boolean(normalizeComparableText(currentDescriptor)) ||
204
+ Boolean(normalizeComparableText(currentVisibleContent)) ||
205
+ generatedExists ||
206
+ cleanupPaths.length > 0,
207
+ currentContent: currentDescriptor,
208
+ nextContent: "",
209
+ diff: createManagedFileRemovalDiff({
210
+ targetPath,
211
+ generatedPath: toDisplayPath(options.cwd, generatedSourcePath, options.statePaths.scope),
212
+ generatedExists,
213
+ currentDescriptor,
214
+ currentContent: currentVisibleContent,
215
+ cleanupPaths: cleanupPaths.map((cleanupPath) => toDisplayPath(options.cwd, cleanupPath, options.statePaths.scope)),
216
+ }),
217
+ syncMode: requestedMode,
218
+ generatedSourcePath,
219
+ };
220
+ }
163
221
  if (requestedMode === "copy") {
164
222
  const existing = (await readText(absoluteTargetPath, "")) || "";
165
223
  return {
@@ -174,7 +232,6 @@ export async function prepareSyncAdapterFiles(options) {
174
232
  syncMode: "copy",
175
233
  };
176
234
  }
177
- const generatedSourcePath = path.join(options.statePaths.generatedDirPath, adapter.id, path.basename(adapter.syncTarget));
178
235
  const currentDescriptor = await describeTarget(absoluteTargetPath);
179
236
  const currentVisibleContent = (await readText(absoluteTargetPath, "")) || "";
180
237
  const nextDescriptor = `symlink -> ${toPosix(path.relative(path.dirname(absoluteTargetPath), generatedSourcePath))}\n`;
@@ -437,6 +494,35 @@ function createManagedFileDiff(context) {
437
494
  }
438
495
  return `${parts.join("\n")}\n`;
439
496
  }
497
+ function createManagedBlockRemovalDiff(context) {
498
+ const parts = [];
499
+ if (normalizeComparableText(context.currentContent) !== "") {
500
+ parts.push(createTextDiff(context.currentContent, "", context.targetPath).trimEnd());
501
+ }
502
+ for (const cleanupPath of context.cleanupPaths) {
503
+ parts.push(`- remove ${cleanupPath}`);
504
+ }
505
+ if (parts.length === 0) {
506
+ return `Sem alteracoes em ${context.targetPath}.\n`;
507
+ }
508
+ return `${parts.join("\n")}\n`;
509
+ }
510
+ function createManagedFileRemovalDiff(context) {
511
+ const parts = [];
512
+ if (normalizeComparableText(context.currentDescriptor) !== "") {
513
+ parts.push(createTextDiff(context.currentDescriptor, "", context.targetPath).trimEnd());
514
+ }
515
+ if (normalizeComparableText(context.currentContent) !== "") {
516
+ parts.push(createTextDiff(context.currentContent, "", context.targetPath).trimEnd());
517
+ }
518
+ if (context.generatedExists) {
519
+ parts.push(`- remove ${context.generatedPath}`);
520
+ }
521
+ for (const cleanupPath of context.cleanupPaths) {
522
+ parts.push(`- remove ${cleanupPath}`);
523
+ }
524
+ return `${parts.join("\n")}\n`;
525
+ }
440
526
  function createTextDiff(currentContent, nextContent, targetPath) {
441
527
  if (normalizeComparableText(currentContent) === normalizeComparableText(nextContent)) {
442
528
  return `Sem alteracoes em ${targetPath}.\n`;
package/dist/types.d.ts CHANGED
@@ -172,6 +172,10 @@ export interface SyncMetadata {
172
172
  syncedAt: string;
173
173
  skillIds?: string[] | undefined;
174
174
  }
175
+ /**
176
+ * Per-adapter synchronization metadata persisted in the lockfile.
177
+ */
178
+ export type SyncHistory = Record<string, SyncMetadata>;
175
179
  /**
176
180
  * Full workspace lockfile structure.
177
181
  */
@@ -183,6 +187,7 @@ export interface LockfileState {
183
187
  adapters: LockfileAdapters;
184
188
  settings: LockfileSettings;
185
189
  sync: SyncMetadata | null;
190
+ syncHistory: SyncHistory;
186
191
  syncMode: SyncWriteMode | null;
187
192
  installed: Record<string, InstalledSkillMetadata>;
188
193
  }
@@ -263,6 +268,7 @@ export interface PreparedSyncResult {
263
268
  absoluteTargetPath: string;
264
269
  targetPath: string;
265
270
  cleanupPaths: string[];
271
+ removeTarget?: boolean | undefined;
266
272
  changed: boolean;
267
273
  currentContent: string;
268
274
  nextContent: string;
@@ -291,6 +297,15 @@ export interface SyncPreview {
291
297
  before: string;
292
298
  after: string;
293
299
  }
300
+ /**
301
+ * Per-adapter sync summary returned to callers.
302
+ */
303
+ export interface SyncExecutionSummary {
304
+ adapter: string;
305
+ targetPath: string;
306
+ syncMode: SyncWriteMode;
307
+ changed: boolean;
308
+ }
294
309
  /**
295
310
  * Public sync result returned after writing or dry-run preparation.
296
311
  */
@@ -325,6 +340,7 @@ export interface SyncCommandResult {
325
340
  adapter: string;
326
341
  targetPath: string;
327
342
  } | SyncMetadata;
343
+ syncs: SyncExecutionSummary[];
328
344
  skillCount: number;
329
345
  changed: boolean;
330
346
  diff: string;