skillex 0.2.3 → 0.2.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.5] - 2026-04-08
11
+
12
+ ### Added
13
+ - `technical-writing-pro` first-party skill for structured technical writing and documentation
14
+
15
+ ### Changed
16
+ - Auto-sync now enabled by default on `skillex init` (was `false`)
17
+ - Auto-sync syncs **all detected adapters**, not only the active one — workspaces with multiple agents (e.g. `.claude/` and `.codex/`) are kept in sync automatically
18
+ - `sync` and auto-sync output now lists each adapter individually with its target path, sync mode, and whether anything changed
19
+ - Removed the restriction that required an active adapter to enable auto-sync
20
+
21
+ ### Fixed
22
+ - Skill removal now cleans up the synced adapter target file and its generated source when the last skill is removed
23
+ - `remove` correctly iterates all resolved adapters when propagating the removal
24
+
25
+ ## [0.2.4] - 2026-04-08
26
+
27
+ ### Changed
28
+ - `skillex ui` skill list now shows compact labels: `Name (id) · tag1, tag2, tag3` — description and full compatibility list removed from each row
29
+ - `skillex ui` shows "Fetching catalog..." while loading and limits visible rows to 12 at a time
30
+ - `skillex install` and `skillex update` now render an inline progress bar (`[████░░░░] 1/5 skill-id`) instead of printing one line per skill
31
+
10
32
  ## [0.2.3] - 2026-04-08
11
33
 
12
34
  ### Fixed
package/README.md CHANGED
@@ -41,13 +41,9 @@ npx skillex@latest list
41
41
 
42
42
  # 3. Install a skill
43
43
  npx skillex@latest install create-skills
44
-
45
- # 4. Write the installed skills into your agent's config file
46
- # ⚠️ This step is required — without it, your agent cannot see the skills.
47
- npx skillex@latest sync
48
44
  ```
49
45
 
50
- > **Important:** `install` only stores skills in Skillex managed state. `sync` is what exposes them to your AI agent. For directory-native adapters such as Codex, Claude, and Gemini, `sync` materializes one folder per skill under the agent's `skills/` directory. For file-based adapters such as Copilot, Cursor, Cline, and Windsurf, `sync` updates the adapter's config file. **You must run `sync` after every install or update for your agent to pick up the changes.** Use `--auto-sync` at `init` time to have this happen automatically.
46
+ > **Important:** auto-sync is enabled by default. After `init`, `install`, `update`, and `remove`, Skillex automatically synchronizes skills into every detected adapter target. For directory-native adapters such as Codex, Claude, and Gemini, this materializes one folder per skill under the agent's `skills/` directory. For file-based adapters such as Copilot, Cursor, Cline, and Windsurf, it updates the adapter config file. Use `skillex sync` when you want to preview, re-run manually, or target a specific adapter.
51
47
 
52
48
  After `init`, Skillex saves the configured source list in the local lockfile. New workspaces start with `lgili/skillex@main` by default, and you can add more sources later with `skillex source add`.
53
49
 
@@ -88,6 +84,7 @@ skillex init
88
84
  skillex init --repo <owner/repo>
89
85
  skillex init --repo lgili/skillex --adapter cursor
90
86
  skillex init --repo lgili/skillex --auto-sync
87
+ skillex init --auto-sync=false
91
88
  skillex init --global --adapter codex
92
89
  ```
93
90
 
@@ -95,7 +92,7 @@ skillex init --global --adapter codex
95
92
  |------|-------------|
96
93
  | `--repo <owner/repo>` | Optional. Overrides the default first-party source for this workspace. |
97
94
  | `--adapter <id>` | Force a specific adapter instead of auto-detecting. |
98
- | `--auto-sync` | Automatically run `sync` after every install, update, and remove. |
95
+ | `--auto-sync` | Enable or disable automatic sync after install, update, and remove. Default: `true`. |
99
96
  | `--ref <branch>` | Use a specific branch or tag (default: `main`). |
100
97
  | `--scope <local\|global>` | Choose whether Skillex manages workspace or user-global state. |
101
98
  | `--global` | Shortcut for `--scope global`. |
@@ -173,7 +170,7 @@ skillex install create-skills --global
173
170
  | `--scope <local\|global>` | Choose whether Skillex writes to `.agent-skills/` or `~/.skillex/`. |
174
171
  | `--global` | Shortcut for `--scope global`. |
175
172
 
176
- > **After installing,** run `skillex sync` to write the skills into your agent's config file. Without this step, the agent will not see the newly installed skills. If you initialized with `--auto-sync`, this happens automatically.
173
+ > **After installing,** Skillex syncs automatically by default. Run `skillex sync` manually only when you want to preview changes, re-run synchronization, or force a specific adapter.
177
174
 
178
175
  ---
179
176
 
@@ -208,10 +205,10 @@ skillex remove create-skills --global
208
205
 
209
206
  ### `sync`
210
207
 
211
- Expose all installed skills to the active adapter. For `codex`, `claude`, and `gemini`, this creates one folder per skill under the adapter's `skills/` directory. For file-based adapters, it updates the adapter's config file. **This is the step that makes skills visible to your AI agent.** Run it after every `install`, `update`, or `remove`.
208
+ Expose all installed skills to the detected adapters. For `codex`, `claude`, and `gemini`, this creates one folder per skill under the adapter's `skills/` directory. For file-based adapters, it updates the adapter's config file. This is the same operation Skillex runs automatically after `install`, `update`, and `remove`.
212
209
 
213
210
  ```bash
214
- # Sync to the detected adapter
211
+ # Sync to all detected adapters
215
212
  skillex sync
216
213
 
217
214
  # Preview changes without writing (shows a diff)
@@ -237,22 +234,17 @@ skillex sync --global --adapter codex
237
234
 
238
235
  #### Using multiple agents in the same workspace
239
236
 
240
- `sync` writes to one adapter at a time. If you use more than one AI agent in the same folder (e.g. Claude and Codex), run `sync` once for each:
237
+ By default, `sync` writes to every detected adapter in the workspace. If you want to limit the operation to one adapter, pass `--adapter`:
241
238
 
242
239
  ```bash
243
- # Write skill folders into .claude/skills/<skill-id>/
240
+ # Only sync Claude
244
241
  skillex sync --adapter claude
245
242
 
246
- # Write skill folders into .codex/skills/<skill-id>/
243
+ # Only sync Codex
247
244
  skillex sync --adapter codex
248
245
  ```
249
246
 
250
- Each adapter writes to its own target path, so the two syncs are independent and non-destructive. To avoid running both commands manually after every change, initialize with `--auto-sync` and then re-run `skillex init --adapter <id>` for each adapter you want covered — or simply alias both commands in your workflow:
251
-
252
- ```bash
253
- # Sync to all agents at once (shell alias / Makefile target)
254
- skillex sync --adapter claude && skillex sync --adapter codex
255
- ```
247
+ Each adapter writes to its own target path, so the syncs are independent and non-destructive. Automatic sync uses the same multi-adapter behavior.
256
248
 
257
249
  ---
258
250
 
@@ -580,14 +572,14 @@ For directory-native adapters, `sync` creates per-skill directories such as:
580
572
 
581
573
  ## Auto-sync
582
574
 
583
- When `--auto-sync` is enabled at `init`, Skillex runs `sync` automatically after every `install`, `update`, and `remove`. This keeps your agent target path always up to date.
575
+ Auto-sync is enabled by default. After `init`, Skillex automatically runs sync after every `install`, `update`, and `remove`, targeting every detected adapter unless you explicitly override one with `--adapter`.
584
576
 
585
577
  ```bash
586
- # Enable at init time
578
+ # Explicitly enable at init time
587
579
  skillex init --auto-sync
588
580
 
589
- # Or re-initialize to enable it
590
- skillex init --repo lgili/skillex --auto-sync
581
+ # Explicitly disable it
582
+ skillex init --auto-sync=false
591
583
 
592
584
  # Enable it for global installs
593
585
  skillex init --global --adapter codex --auto-sync
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ Options:
21
21
  --repo <owner/repo> GitHub repository with skills (default: lgili/skillex)
22
22
  --ref <ref> Branch, tag, or commit (default: main)
23
23
  --adapter <id> Force a specific adapter
24
- --auto-sync Enable auto-sync after install/update/remove
24
+ --auto-sync Enable or disable auto-sync (default: on)
25
25
  --scope <scope> local or global (default: local)
26
26
  --global Shortcut for --scope global
27
27
  --cwd <path> Target project directory (default: current directory)
@@ -296,6 +296,9 @@ async function handleInit(flags, userConfig) {
296
296
  ` Use --adapter <id> to specify one. Available: ${listAdapters().map((a) => a.id).join(", ")}`);
297
297
  }
298
298
  output.info(` Auto-sync: ${result.lockfile.settings.autoSync ? "enabled" : "disabled"}`);
299
+ if (result.lockfile.adapters.detected.length > 0) {
300
+ output.info(` Detected : ${result.lockfile.adapters.detected.join(", ")}`);
301
+ }
299
302
  output.info("\nNext: run 'skillex list' to browse available skills");
300
303
  }
301
304
  async function handleList(flags, userConfig) {
@@ -362,11 +365,9 @@ async function handleInstall(positionals, flags, userConfig) {
362
365
  const result = await installSkills(positionals, {
363
366
  ...opts,
364
367
  installAll,
365
- onProgress: (current, total, skillId) => {
366
- output.info(`[${current}/${total}] Installing ${skillId}...`);
367
- },
368
+ onProgress: (current, total, skillId) => output.progress(current, total, skillId),
368
369
  });
369
- output.success(`Installed ${result.installedCount} skill(s) to ${result.statePaths.scope} state at ${result.statePaths.stateDir}`);
370
+ output.success(`Installed ${result.installedCount} skill(s)`);
370
371
  for (const skill of result.installedSkills) {
371
372
  output.info(` + ${skill.id}@${skill.version}`);
372
373
  }
@@ -374,7 +375,10 @@ async function handleInstall(positionals, flags, userConfig) {
374
375
  }
375
376
  async function handleUpdate(positionals, flags, userConfig) {
376
377
  const opts = commonOptions(flags, userConfig);
377
- const result = await updateInstalledSkills(positionals, opts);
378
+ const result = await updateInstalledSkills(positionals, {
379
+ ...opts,
380
+ onProgress: (current, total, skillId) => output.progress(current, total, skillId),
381
+ });
378
382
  if (result.updatedSkills.length === 0) {
379
383
  output.info("No skills updated.");
380
384
  }
@@ -405,17 +409,19 @@ async function handleRemove(positionals, flags, userConfig) {
405
409
  async function handleSync(flags, userConfig) {
406
410
  const result = await syncInstalledSkills(commonOptions(flags, userConfig));
407
411
  if (result.dryRun) {
408
- output.info(`Preview: ${result.skillCount} skill(s) → ${result.sync.adapter}`);
409
- output.info(`Target path : ${result.sync.targetPath}`);
410
- output.info(`Sync mode : ${result.syncMode}`);
412
+ output.info(`Preview: ${result.skillCount} skill(s)`);
413
+ for (const entry of result.syncs) {
414
+ output.info(` ${entry.adapter} ${entry.targetPath} [${entry.syncMode}]${entry.changed ? "" : " (no changes)"}`);
415
+ }
411
416
  process.stdout.write(result.diff);
412
417
  return;
413
418
  }
414
- output.success(`Synced ${result.skillCount} skill(s) → ${result.sync.adapter}`);
415
- output.info(`Target path : ${result.sync.targetPath}`);
416
- output.info(`Sync mode : ${result.syncMode}`);
419
+ output.success(`Synced ${result.skillCount} skill(s)`);
420
+ for (const entry of result.syncs) {
421
+ output.info(` ${entry.adapter} ${entry.targetPath} [${entry.syncMode}]${entry.changed ? "" : " (no changes)"}`);
422
+ }
417
423
  if (!result.changed) {
418
- output.info("No changes to the target path.");
424
+ output.info("No changes to the target paths.");
419
425
  }
420
426
  }
421
427
  async function handleRun(positionals, flags, userConfig) {
@@ -437,7 +443,9 @@ async function handleUi(flags, userConfig) {
437
443
  const options = commonOptions(flags, userConfig);
438
444
  const state = await getInstalledSkills(options);
439
445
  const source = await resolveProjectSource(options);
446
+ output.statusLine("Fetching catalog...");
440
447
  const catalog = await loadCatalog({ ...source, ...cacheOptions(options) });
448
+ output.clearStatus();
441
449
  if (catalog.skills.length === 0) {
442
450
  output.info("No skills available in the catalog.");
443
451
  return;
@@ -450,19 +458,24 @@ async function handleUi(flags, userConfig) {
450
458
  : "No skills available in the catalog.");
451
459
  return;
452
460
  }
453
- const installResult = selection.toInstall.length > 0 ? await installSkills(selection.toInstall, options) : null;
461
+ const installResult = selection.toInstall.length > 0
462
+ ? await installSkills(selection.toInstall, {
463
+ ...options,
464
+ onProgress: (current, total, skillId) => output.progress(current, total, skillId),
465
+ })
466
+ : null;
454
467
  const removeResult = selection.toRemove.length > 0 ? await removeSkills(selection.toRemove, options) : null;
455
468
  if (!installResult && !removeResult) {
456
469
  output.info("No changes applied.");
457
470
  return;
458
471
  }
459
- output.success("UI summary:");
460
472
  if (installResult) {
461
- output.info(` Installed : ${installResult.installedSkills.map((s) => s.id).join(", ")}`);
473
+ output.success(`Installed: ${installResult.installedSkills.map((s) => s.id).join(", ")}`);
462
474
  }
463
475
  if (removeResult) {
464
- output.info(` Removed : ${removeResult.removedSkills.join(", ")}`);
476
+ output.success(`Removed: ${removeResult.removedSkills.join(", ")}`);
465
477
  }
478
+ printAutoSyncResult(installResult?.autoSync ?? removeResult?.autoSync ?? null);
466
479
  }
467
480
  async function handleStatus(flags, userConfig) {
468
481
  const options = commonOptions(flags, userConfig);
@@ -925,8 +938,9 @@ function parseSyncMode(value) {
925
938
  function printAutoSyncResult(result) {
926
939
  if (!result)
927
940
  return;
928
- const suffix = result.changed ? "" : " (no changes)";
929
- output.info(`Auto-sync: ${result.sync.adapter} → ${result.sync.targetPath} [${result.syncMode}]${suffix}`);
941
+ for (const entry of result.syncs) {
942
+ output.info(`Sync: ${entry.adapter} → ${entry.targetPath} [${entry.syncMode}]${entry.changed ? "" : " (no changes)"}`);
943
+ }
930
944
  }
931
945
  function asOptionalString(value) {
932
946
  return typeof value === "string" ? value : undefined;
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) {
package/dist/output.d.ts CHANGED
@@ -44,3 +44,22 @@ export declare function error(message: string): void;
44
44
  * @param message - Debug message.
45
45
  */
46
46
  export declare function debug(message: string): void;
47
+ /**
48
+ * Renders an inline progress bar that overwrites the current line.
49
+ * Prints a newline when current === total.
50
+ *
51
+ * @param current - Number of completed items (1-based).
52
+ * @param total - Total number of items.
53
+ * @param label - Short label shown after the bar.
54
+ */
55
+ export declare function progress(current: number, total: number, label: string): void;
56
+ /**
57
+ * Writes a transient status message on the current line (overwritable with clearStatus).
58
+ *
59
+ * @param message - Status message to display.
60
+ */
61
+ export declare function statusLine(message: string): void;
62
+ /**
63
+ * Clears the current status line written by {@link statusLine}.
64
+ */
65
+ export declare function clearStatus(): void;
package/dist/output.js CHANGED
@@ -76,3 +76,46 @@ export function debug(message) {
76
76
  process.stderr.write(applyColor("2", `[debug] ${message}`, process.stderr) + "\n");
77
77
  }
78
78
  }
79
+ // ---------------------------------------------------------------------------
80
+ // Progress and status helpers (TTY-only; fall back to plain lines otherwise)
81
+ // ---------------------------------------------------------------------------
82
+ /**
83
+ * Renders an inline progress bar that overwrites the current line.
84
+ * Prints a newline when current === total.
85
+ *
86
+ * @param current - Number of completed items (1-based).
87
+ * @param total - Total number of items.
88
+ * @param label - Short label shown after the bar.
89
+ */
90
+ export function progress(current, total, label) {
91
+ if (!process.stdout.isTTY) {
92
+ console.log(`[${current}/${total}] ${label}`);
93
+ return;
94
+ }
95
+ const filled = total > 0 ? Math.round((current / total) * 16) : 0;
96
+ const bar = applyColor("32", "█".repeat(filled), process.stdout) + "░".repeat(16 - filled);
97
+ const counter = applyColor("2", `${current}/${total}`, process.stdout);
98
+ const line = ` [${bar}] ${counter} ${label}`;
99
+ process.stdout.write(`\r${line}\x1b[K`);
100
+ if (current === total) {
101
+ process.stdout.write("\n");
102
+ }
103
+ }
104
+ /**
105
+ * Writes a transient status message on the current line (overwritable with clearStatus).
106
+ *
107
+ * @param message - Status message to display.
108
+ */
109
+ export function statusLine(message) {
110
+ if (!process.stdout.isTTY)
111
+ return;
112
+ process.stdout.write(`\r ${applyColor("2", message, process.stdout)}\x1b[K`);
113
+ }
114
+ /**
115
+ * Clears the current status line written by {@link statusLine}.
116
+ */
117
+ export function clearStatus() {
118
+ if (!process.stdout.isTTY)
119
+ return;
120
+ process.stdout.write("\r\x1b[K");
121
+ }
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;
package/dist/ui.d.ts CHANGED
@@ -12,6 +12,7 @@ interface UiPrompts {
12
12
  checkbox?: ((options: {
13
13
  message: string;
14
14
  instructions?: string | undefined;
15
+ pageSize?: number | undefined;
15
16
  choices: UiChoice[];
16
17
  }) => Promise<string[]>) | undefined;
17
18
  }
package/dist/ui.js CHANGED
@@ -33,13 +33,21 @@ export async function runInteractiveUi(options) {
33
33
  const selectedIds = filteredSkills.length === 0
34
34
  ? []
35
35
  : await (prompts.checkbox || fallbackCheckbox)({
36
- message: "Selecione as skills",
37
- instructions: "Type to filter first • Space to select Enter to install",
38
- choices: filteredSkills.map((skill) => ({
39
- name: `${skill.name} (${skill.id}) - ${skill.description || "Sem descricao"} [${skill.compatibility.join(",") || "sem-compat"}]`,
40
- value: skill.id,
41
- checked: installedSet.has(skill.id),
42
- })),
36
+ message: "Select skills",
37
+ instructions: "↑↓ navigate · space select · enter confirm · type to filter",
38
+ pageSize: 12,
39
+ choices: filteredSkills.map((skill) => {
40
+ const tags = (skill.tags ?? []).slice(0, 4).join(", ");
41
+ const detail = tags || (skill.description ?? "").slice(0, 55);
42
+ const label = detail
43
+ ? `${skill.name} (${skill.id}) · ${detail}`
44
+ : `${skill.name} (${skill.id})`;
45
+ return {
46
+ name: label,
47
+ value: skill.id,
48
+ checked: installedSet.has(skill.id),
49
+ };
50
+ }),
43
51
  });
44
52
  const selectedSet = new Set(selectedIds);
45
53
  const toInstall = selectedIds.filter((skillId) => !installedSet.has(skillId));
@@ -62,6 +70,7 @@ async function loadPromptAdapters() {
62
70
  checkbox: async (options) => prompts.checkbox({
63
71
  message: options.message,
64
72
  ...(options.instructions ? { instructions: options.instructions } : {}),
73
+ ...(options.pageSize !== undefined ? { pageSize: options.pageSize } : {}),
65
74
  choices: options.choices.map((choice) => ({
66
75
  name: choice.name,
67
76
  value: choice.value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillex",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "CLI to list, install, and synchronize AI agent skills from GitHub-hosted catalogs.",
5
5
  "type": "module",
6
6
  "repository": {