@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 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`. Pass `--yes` to skip the prompt in CI scripts.
66
+ The `last_sync_at` watermark persists at `~/.prave/state.json`.
67
67
 
68
68
  ## Commands
69
69
 
@@ -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
- let entries = [];
116
- try {
117
- entries = await readdir(CONFIG.skillsDir);
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
- catch {
120
- spinner.warn(`No Skills directory at ${CONFIG.skillsDir}`);
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
- const targetDir = join(CONFIG.skillsDir, item.slug);
185
- try {
186
- await mkdir(targetDir, { recursive: true });
187
- await writeFile(join(targetDir, 'SKILL.md'), item.content, 'utf8');
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
- catch {
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.4.15",
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.15"
57
+ "@prave/shared": "1.4.16"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^20.12.7",