@prave/cli 1.4.15 → 1.5.1
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/README.md +1 -1
- package/dist/commands/sync.js +53 -21
- package/dist/lib/agent-paths.js +56 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Syncing 12 Skills — this takes about 15 seconds.
|
|
|
63
63
|
Synced 12 · failed 0
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
The `last_sync_at` watermark persists at `~/.prave/state.json`.
|
|
66
|
+
The `last_sync_at` watermark persists at `~/.prave/state.json`.
|
|
67
67
|
|
|
68
68
|
## Commands
|
|
69
69
|
|
package/dist/commands/sync.js
CHANGED
|
@@ -3,9 +3,9 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import { resolveAgentTargets } from '../lib/agent-paths.js';
|
|
6
7
|
import { track } from '../lib/analytics.js';
|
|
7
8
|
import { api, ApiError } from '../lib/api.js';
|
|
8
|
-
import { CONFIG } from '../lib/config.js';
|
|
9
9
|
import { requireAuth } from '../lib/credentials.js';
|
|
10
10
|
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
11
11
|
import { readState, writeState } from '../lib/state.js';
|
|
@@ -111,28 +111,49 @@ export async function syncCommand() {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
// Resolve every on-disk target the user has enabled in web settings.
|
|
115
|
+
// Pre-2026-06-05 sync was hard-pinned to ~/.claude/skills/ — users
|
|
116
|
+
// with Codex / Cline / Amp enabled saw nothing land in those dirs.
|
|
117
|
+
// Now we read the server-side agent settings and write to every
|
|
118
|
+
// enabled (non-conversion) path. Cursor is excluded for now because
|
|
119
|
+
// it needs format conversion.
|
|
120
|
+
const targets = await resolveAgentTargets();
|
|
121
|
+
const targetSummary = targets.map((t) => t.agent).join(', ');
|
|
114
122
|
const spinner = ora('Scanning local Skills…').start();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Union of slugs across all enabled targets — a skill that only
|
|
124
|
+
// exists in ~/.claude/skills is still "installed", and one we
|
|
125
|
+
// pulled into ~/.codex/skills the last sync should re-sync too.
|
|
126
|
+
const slugSet = new Set();
|
|
127
|
+
let scannedAny = false;
|
|
128
|
+
for (const target of targets) {
|
|
129
|
+
let entries = [];
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(target.dir);
|
|
132
|
+
scannedAny = true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Missing dir → user enabled the agent in settings but never
|
|
136
|
+
// installed a skill there. Sync will create it on first write.
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const name of entries) {
|
|
140
|
+
const dir = join(target.dir, name);
|
|
141
|
+
if ((await stat(dir).catch(() => null))?.isDirectory())
|
|
142
|
+
slugSet.add(name);
|
|
143
|
+
}
|
|
118
144
|
}
|
|
119
|
-
|
|
120
|
-
spinner.warn(`No Skills directory
|
|
145
|
+
if (!scannedAny && slugSet.size === 0) {
|
|
146
|
+
spinner.warn(`No Skills directory found across ${targetSummary || 'claude'}.`);
|
|
121
147
|
return;
|
|
122
148
|
}
|
|
123
|
-
const slugs =
|
|
124
|
-
for (const name of entries) {
|
|
125
|
-
const dir = join(CONFIG.skillsDir, name);
|
|
126
|
-
if ((await stat(dir).catch(() => null))?.isDirectory())
|
|
127
|
-
slugs.push(name);
|
|
128
|
-
}
|
|
149
|
+
const slugs = Array.from(slugSet);
|
|
129
150
|
if (!slugs.length) {
|
|
130
151
|
spinner.warn('No installed Skills to sync.');
|
|
131
152
|
return;
|
|
132
153
|
}
|
|
133
|
-
spinner.succeed(`Found ${slugs.length} installed Skill${slugs.length === 1 ? '' : 's'}.`);
|
|
154
|
+
spinner.succeed(`Found ${slugs.length} installed Skill${slugs.length === 1 ? '' : 's'} across ${targetSummary || 'claude'}.`);
|
|
134
155
|
const estSeconds = estimateSeconds(slugs.length);
|
|
135
|
-
console.log(chalk.dim(`Syncing ${slugs.length} Skill${slugs.length === 1 ? '' : 's'} — this takes about ${estSeconds} seconds.`));
|
|
156
|
+
console.log(chalk.dim(`Syncing ${slugs.length} Skill${slugs.length === 1 ? '' : 's'} → ${targets.length} agent dir${targets.length === 1 ? '' : 's'} — this takes about ${estSeconds} seconds.`));
|
|
136
157
|
const progress = ora(`Synced 0 / ${slugs.length}`).start();
|
|
137
158
|
let updated = 0;
|
|
138
159
|
let paywalled = 0;
|
|
@@ -170,7 +191,10 @@ export async function syncCommand() {
|
|
|
170
191
|
// Track per-slug verdicts for the final summary line.
|
|
171
192
|
missing += response.missing.length;
|
|
172
193
|
missingSlugs.push(...response.missing);
|
|
173
|
-
// Write files in parallel, bounded.
|
|
194
|
+
// Write files in parallel, bounded. Each successful item gets
|
|
195
|
+
// written to EVERY target dir (claude + codex + …) — failures on
|
|
196
|
+
// a single target don't abort the others; we just count a skill
|
|
197
|
+
// as updated when at least one write succeeded.
|
|
174
198
|
await runWithConcurrency(response.items, WRITE_CONCURRENCY, async (item) => {
|
|
175
199
|
if (item.error === 'paid_unpurchased') {
|
|
176
200
|
paywalled += 1;
|
|
@@ -181,15 +205,23 @@ export async function syncCommand() {
|
|
|
181
205
|
noContent += 1;
|
|
182
206
|
return;
|
|
183
207
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
let anyWritten = false;
|
|
209
|
+
for (const target of targets) {
|
|
210
|
+
const dir = join(target.dir, item.slug);
|
|
211
|
+
try {
|
|
212
|
+
await mkdir(dir, { recursive: true });
|
|
213
|
+
await writeFile(join(dir, 'SKILL.md'), item.content, 'utf8');
|
|
214
|
+
anyWritten = true;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* per-target write failure — continue with other targets */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (anyWritten) {
|
|
188
221
|
updated += 1;
|
|
189
222
|
progress.text = `Synced ${updated} / ${slugs.length} — ${item.slug}`;
|
|
190
223
|
}
|
|
191
|
-
|
|
192
|
-
// Disk-level failure — surface in the summary, don't crash.
|
|
224
|
+
else {
|
|
193
225
|
noContent += 1;
|
|
194
226
|
}
|
|
195
227
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { AGENT_REGISTRY } from '@prave/shared';
|
|
3
|
+
import { api, ApiError } from './api.js';
|
|
4
|
+
import { CONFIG } from './config.js';
|
|
5
|
+
const isWindows = process.platform === 'win32';
|
|
6
|
+
function expandTilde(p) {
|
|
7
|
+
if (!p)
|
|
8
|
+
return p;
|
|
9
|
+
if (p === '~')
|
|
10
|
+
return homedir();
|
|
11
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) {
|
|
12
|
+
return homedir() + p.slice(1);
|
|
13
|
+
}
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
function pickOsPath(paths) {
|
|
17
|
+
return isWindows ? paths.windows : paths.mac;
|
|
18
|
+
}
|
|
19
|
+
export async function resolveAgentTargets() {
|
|
20
|
+
try {
|
|
21
|
+
const { data: settings } = await api.get('/api/v1/settings/agents', true);
|
|
22
|
+
const enabled = settings.enabled_agents ?? [];
|
|
23
|
+
if (enabled.length === 0) {
|
|
24
|
+
return [{ agent: 'claude', dir: CONFIG.skillsDir }];
|
|
25
|
+
}
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const targets = [];
|
|
28
|
+
for (const agent of enabled) {
|
|
29
|
+
const meta = AGENT_REGISTRY[agent];
|
|
30
|
+
if (!meta)
|
|
31
|
+
continue;
|
|
32
|
+
if (meta.conversionRequired)
|
|
33
|
+
continue;
|
|
34
|
+
const stored = settings.skill_paths?.[agent];
|
|
35
|
+
const raw = stored
|
|
36
|
+
? pickOsPath(stored)
|
|
37
|
+
: pickOsPath(meta.defaultPath);
|
|
38
|
+
const dir = expandTilde(raw).replace(/[\\/]+$/, '');
|
|
39
|
+
if (!dir || seen.has(dir))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(dir);
|
|
42
|
+
targets.push({ agent, dir });
|
|
43
|
+
}
|
|
44
|
+
if (targets.length === 0) {
|
|
45
|
+
return [{ agent: 'claude', dir: CONFIG.skillsDir }];
|
|
46
|
+
}
|
|
47
|
+
return targets;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// Fail-safe path: legacy single-claude behaviour. A 401 here just
|
|
51
|
+
// means the user isn't authed and the caller will surface that
|
|
52
|
+
// through requireAuth() anyway.
|
|
53
|
+
void (err instanceof ApiError);
|
|
54
|
+
return [{ agent: 'claude', dir: CONFIG.skillsDir }];
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Prave CLI — discover, install, version, test, and ship Claude Skills. The developer platform for the complete Skill lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"ora": "^8.0.1",
|
|
55
55
|
"tar": "^7.4.3",
|
|
56
56
|
"undici": "^6.18.0",
|
|
57
|
-
"@prave/shared": "1.4.
|
|
57
|
+
"@prave/shared": "1.4.16"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^20.12.7",
|