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 +22 -0
- package/README.md +14 -22
- package/dist/cli.js +33 -19
- package/dist/install.js +148 -35
- package/dist/output.d.ts +19 -0
- package/dist/output.js +43 -0
- package/dist/sync.js +90 -4
- package/dist/types.d.ts +16 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +16 -7
- package/package.json +1 -1
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:**
|
|
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` |
|
|
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,**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
240
|
+
# Only sync Claude
|
|
244
241
|
skillex sync --adapter claude
|
|
245
242
|
|
|
246
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
578
|
+
# Explicitly enable at init time
|
|
587
579
|
skillex init --auto-sync
|
|
588
580
|
|
|
589
|
-
#
|
|
590
|
-
skillex init --
|
|
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
|
|
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)
|
|
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,
|
|
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)
|
|
409
|
-
|
|
410
|
-
|
|
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)
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
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
|
|
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.
|
|
473
|
+
output.success(`Installed: ${installResult.installedSkills.map((s) => s.id).join(", ")}`);
|
|
462
474
|
}
|
|
463
475
|
if (removeResult) {
|
|
464
|
-
output.
|
|
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
|
|
929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
274
|
+
const autoSync = await maybeSyncAfterRemove(withAgentSkillsDir({
|
|
276
275
|
cwd,
|
|
277
276
|
scope: options.scope,
|
|
278
|
-
|
|
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
|
|
316
|
-
if (
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
338
|
-
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:
|
|
342
|
-
diff:
|
|
361
|
+
changed: syncResults.some((result) => result.changed),
|
|
362
|
+
diff: diffParts.length > 0 ? `${diffParts.join("\n\n")}\n` : "",
|
|
343
363
|
dryRun: true,
|
|
344
|
-
syncMode:
|
|
364
|
+
syncMode: primarySync.syncMode,
|
|
345
365
|
};
|
|
346
366
|
}
|
|
347
|
-
|
|
348
|
-
adapter:
|
|
349
|
-
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
|
-
|
|
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:
|
|
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:
|
|
361
|
-
diff:
|
|
400
|
+
changed: syncResults.some((result) => result.changed),
|
|
401
|
+
diff: diffParts.length > 0 ? `${diffParts.join("\n\n")}\n` : "",
|
|
362
402
|
dryRun: false,
|
|
363
|
-
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
148
|
-
|
|
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
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: "
|
|
37
|
-
instructions: "
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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,
|