@prave/cli 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/deploy.js +10 -0
- package/dist/commands/mcp-server.js +13 -2
- package/dist/commands/run.js +49 -5
- package/dist/index.js +16 -32
- package/dist/lib/api.js +55 -26
- package/package.json +2 -2
- package/dist/commands/export.js +0 -35
package/dist/commands/deploy.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-agent fan-out helper.
|
|
3
|
+
*
|
|
4
|
+
* This module used to back a public `prave deploy <skill>` command, but
|
|
5
|
+
* shipping that as a top-level CLI surface only duplicated what
|
|
6
|
+
* `prave install` already prompts for ("Deploy to all configured agents?
|
|
7
|
+
* [y/N]"). The standalone command was removed; `deployCommand` lives on
|
|
8
|
+
* as an internal helper that `install.ts` and `sync.ts` import to do
|
|
9
|
+
* the actual fan-out + Cursor `.mdc` rewrite.
|
|
10
|
+
*/
|
|
1
11
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
12
|
import { homedir } from 'node:os';
|
|
3
13
|
import { join } from 'node:path';
|
|
@@ -118,8 +118,16 @@ async function handleSearch(args) {
|
|
|
118
118
|
if (args.category)
|
|
119
119
|
params.set('category', args.category);
|
|
120
120
|
params.set('limit', String(Math.min(20, Math.max(1, args.limit ?? 10))));
|
|
121
|
+
// The API's GET /skills returns a flat array as `data` (the
|
|
122
|
+
// controller calls `res.json(ok(skills))` with skills being
|
|
123
|
+
// `SkillSearchHit[]`). An earlier version of this MCP handler
|
|
124
|
+
// wrapped the response in `{items}` which is the shape the wizard
|
|
125
|
+
// uses, but never the public list endpoint — that mismatch silently
|
|
126
|
+
// turned every search into "No Skills matched" because `data.items`
|
|
127
|
+
// was always undefined. Normalise both shapes to stay forward-
|
|
128
|
+
// compatible with future API shifts.
|
|
121
129
|
const { data } = await api.get(`/api/v1/skills?${params.toString()}`, true);
|
|
122
|
-
const items = data
|
|
130
|
+
const items = Array.isArray(data) ? data : (data?.items ?? []);
|
|
123
131
|
const lines = items.map((s) => `• ${s.name} (${s.slug}) — ${s.description ?? 'no description'} · ↓${s.install_count}`);
|
|
124
132
|
return mcpText(lines.length
|
|
125
133
|
? `Found ${lines.length} Skill${lines.length === 1 ? '' : 's'}:\n\n${lines.join('\n')}`
|
|
@@ -139,8 +147,11 @@ async function handleWhatdoes(args) {
|
|
|
139
147
|
.join('\n'));
|
|
140
148
|
}
|
|
141
149
|
async function handleMySkills() {
|
|
150
|
+
// Same shape gotcha as handleSearch — /me/skills also returns a flat
|
|
151
|
+
// array via `ok(skills)` where skills is `SkillSearchHit[]`. Normalise
|
|
152
|
+
// both shapes so a future server-side refactor doesn't break the tool.
|
|
142
153
|
const { data } = await api.get('/api/v1/me/skills', true);
|
|
143
|
-
const items = data
|
|
154
|
+
const items = Array.isArray(data) ? data : (data?.items ?? []);
|
|
144
155
|
if (items.length === 0) {
|
|
145
156
|
return mcpText('You haven\'t installed or authored any Skills yet. Try `prave_search_skills` to browse the catalogue.');
|
|
146
157
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -47,7 +47,8 @@ const TAR_IGNORE = new Set([
|
|
|
47
47
|
]);
|
|
48
48
|
const MAX_BUNDLE_BYTES = 20 * 1024 * 1024; // matches API + Supabase Storage cap
|
|
49
49
|
const MAX_FILES = 200;
|
|
50
|
-
export async function runDeployCommand(pathArg) {
|
|
50
|
+
export async function runDeployCommand(pathArg, options = {}) {
|
|
51
|
+
const isUpdate = Boolean(options.updateRunSlug);
|
|
51
52
|
const root = resolve(pathArg ?? process.cwd());
|
|
52
53
|
const rootStat = await stat(root).catch(() => null);
|
|
53
54
|
if (!rootStat?.isDirectory()) {
|
|
@@ -107,11 +108,16 @@ export async function runDeployCommand(pathArg) {
|
|
|
107
108
|
'will prompt you for the real values during the wizard.'));
|
|
108
109
|
process.exit(1);
|
|
109
110
|
}
|
|
110
|
-
// 2. Mint the deploy session
|
|
111
|
-
|
|
111
|
+
// 2. Mint the deploy session. Update flows tag the session with
|
|
112
|
+
// the existing run's slug so the upload handler swaps that run's
|
|
113
|
+
// bundle pointer instead of creating a fresh row.
|
|
114
|
+
const initSpinner = ora(isUpdate ? `Opening update session for ${options.updateRunSlug}…` : 'Opening deploy session…').start();
|
|
112
115
|
let session;
|
|
113
116
|
try {
|
|
114
|
-
const
|
|
117
|
+
const initPath = isUpdate
|
|
118
|
+
? `/api/v1/deploy/init?run_slug=${encodeURIComponent(options.updateRunSlug)}`
|
|
119
|
+
: '/api/v1/deploy/init';
|
|
120
|
+
const { data } = await api.post(initPath, {}, true);
|
|
115
121
|
session = data.session;
|
|
116
122
|
initSpinner.succeed('Session opened');
|
|
117
123
|
}
|
|
@@ -178,12 +184,23 @@ export async function runDeployCommand(pathArg) {
|
|
|
178
184
|
process.exit(1);
|
|
179
185
|
}
|
|
180
186
|
uploadSpinner.succeed('Upload complete');
|
|
187
|
+
// Update flow: the server already swapped the run's bundle
|
|
188
|
+
// pointer. No wizard, no browser open — just confirm + exit.
|
|
189
|
+
const parsed = safeJson(text);
|
|
190
|
+
if (isUpdate || parsed?.updated_run_slug) {
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(chalk.bold(`Updated ${parsed?.updated_run_slug ?? options.updateRunSlug}.`));
|
|
193
|
+
console.log(chalk.dim(' Next scheduled fire will use the freshly-uploaded bundle.\n' +
|
|
194
|
+
' Open the dashboard:'));
|
|
195
|
+
console.log(chalk.cyan(' ' + session.wizard_url));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
181
198
|
}
|
|
182
199
|
catch (err) {
|
|
183
200
|
uploadSpinner.fail(`Upload failed: ${err.message}`);
|
|
184
201
|
process.exit(1);
|
|
185
202
|
}
|
|
186
|
-
// 5.
|
|
203
|
+
// 5. New-run flow: open the browser at the wizard.
|
|
187
204
|
console.log();
|
|
188
205
|
console.log(chalk.bold('Finish in the browser:'));
|
|
189
206
|
console.log(chalk.cyan(' ' + session.wizard_url));
|
|
@@ -435,3 +452,30 @@ function safeJson(text) {
|
|
|
435
452
|
return null;
|
|
436
453
|
}
|
|
437
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* `prave run trigger <slug>` — fire a single ad-hoc execution outside
|
|
457
|
+
* the cron schedule. Refused with 409 when another execution is in
|
|
458
|
+
* flight (same single-flight guard the dashboard's "Run now" button
|
|
459
|
+
* uses). Returns immediately after enqueueing — tail with
|
|
460
|
+
* `prave run logs <slug>` after a few seconds.
|
|
461
|
+
*/
|
|
462
|
+
export async function runTriggerCommand(slug) {
|
|
463
|
+
if (!(await requireAuth('prave run trigger')))
|
|
464
|
+
return;
|
|
465
|
+
const spinner = ora(`Triggering ${slug}…`).start();
|
|
466
|
+
try {
|
|
467
|
+
await api.post(`/api/v1/runs/${encodeURIComponent(slug)}/trigger`, undefined, true);
|
|
468
|
+
spinner.succeed(`Triggered ${chalk.bold(slug)} — log will appear in a few seconds.`);
|
|
469
|
+
console.log(chalk.dim(` Tail it with `) +
|
|
470
|
+
chalk.cyan(`prave run logs ${slug}`) +
|
|
471
|
+
chalk.dim(` (or watch the dashboard).`));
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
475
|
+
spinner.warn(`An execution is already in flight for ${slug} — wait for it to finish, then re-run.`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
spinner.fail(err.message);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,8 @@ import { dirname, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { conflictsCommand } from './commands/conflicts.js';
|
|
7
|
-
import { deployCommand } from './commands/deploy.js';
|
|
8
7
|
import { diffCommand } from './commands/diff.js';
|
|
9
8
|
import { docsCommand } from './commands/docs.js';
|
|
10
|
-
import { exportCommand } from './commands/export.js';
|
|
11
9
|
import { importCommand } from './commands/import.js';
|
|
12
10
|
import { installCommand } from './commands/install.js';
|
|
13
11
|
import { listCommand } from './commands/list.js';
|
|
@@ -17,13 +15,13 @@ import { mcpInstallCommand } from './commands/mcp-install.js';
|
|
|
17
15
|
import { mcpServerCommand } from './commands/mcp-server.js';
|
|
18
16
|
import { optimizeCommand } from './commands/optimize.js';
|
|
19
17
|
import { overviewCommand } from './commands/overview.js';
|
|
20
|
-
import { runDeployCommand, runListCommand, runLogsCommand, } from './commands/run.js';
|
|
18
|
+
import { runDeployCommand, runListCommand, runLogsCommand, runTriggerCommand, } from './commands/run.js';
|
|
21
19
|
import { searchCommand } from './commands/search.js';
|
|
22
20
|
import { settingsCommand } from './commands/settings.js';
|
|
23
21
|
import { syncCommand } from './commands/sync.js';
|
|
24
22
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
25
23
|
import { updateCommand } from './commands/update.js';
|
|
26
|
-
import { usageHookInstallCommand, usageHookUninstallCommand, usageReportCommand,
|
|
24
|
+
import { usageHookInstallCommand, usageHookUninstallCommand, usageReportCommand, } from './commands/usage.js';
|
|
27
25
|
import { whatdoesCommand } from './commands/whatdoes.js';
|
|
28
26
|
import { whoamiCommand } from './commands/whoami.js';
|
|
29
27
|
import { initAnalytics } from './lib/analytics.js';
|
|
@@ -36,12 +34,13 @@ const program = new Command()
|
|
|
36
34
|
'Prave — Developer platform for Claude Skills.',
|
|
37
35
|
'',
|
|
38
36
|
' Lifecycle: discover · install · author · sync · audit · ship.',
|
|
39
|
-
' Targets ~/.claude/skills/ by default; multi-agent
|
|
37
|
+
' Targets ~/.claude/skills/ by default; multi-agent fan-out happens at install time.',
|
|
40
38
|
'',
|
|
41
39
|
' Quick start:',
|
|
42
40
|
' prave login # device-code auth in your browser',
|
|
43
41
|
' prave search <q> # discover community skills',
|
|
44
|
-
' prave install <slug> # pull into ~/.claude/skills/',
|
|
42
|
+
' prave install <slug> # pull into ~/.claude/skills/ (multi-agent prompt included)',
|
|
43
|
+
' prave run deploy # bundle cwd, schedule on Prave\'s sandbox',
|
|
45
44
|
' prave usage hook install # real-time invocation tracking',
|
|
46
45
|
'',
|
|
47
46
|
' Docs: https://prave.app/docs',
|
|
@@ -97,11 +96,6 @@ program
|
|
|
97
96
|
.option('--heavy', 'narrow to Skills above the heavy-tier threshold (>5k estimated tokens)')
|
|
98
97
|
.action(listCommand);
|
|
99
98
|
program.command('search <query>').description('Search public Skills').action(searchCommand);
|
|
100
|
-
program
|
|
101
|
-
.command('export <slug>')
|
|
102
|
-
.description('Print or save a Skill\'s SKILL.md without installing')
|
|
103
|
-
.option('-o, --out <file>', 'write to file instead of stdout')
|
|
104
|
-
.action(exportCommand);
|
|
105
99
|
program
|
|
106
100
|
.command('diff <slug>')
|
|
107
101
|
.description('Show local vs registry diff for an installed Skill')
|
|
@@ -135,16 +129,6 @@ program
|
|
|
135
129
|
const usage = program
|
|
136
130
|
.command('usage')
|
|
137
131
|
.description('Track which Skills you actually use (powers the optimiser)');
|
|
138
|
-
usage
|
|
139
|
-
.command('scan')
|
|
140
|
-
.description('Scan local Claude Code transcripts and report invocations to Prave')
|
|
141
|
-
.option('--since <window>', 'override the watermark, e.g. "7d" or "12h"')
|
|
142
|
-
.option('--quiet', 'log a one-liner instead of a spinner')
|
|
143
|
-
.action(usageScanCommand);
|
|
144
|
-
usage
|
|
145
|
-
.command('status')
|
|
146
|
-
.description('Diagnose hook health, recent event counts, and most-used Skills')
|
|
147
|
-
.action(usageStatusCommand);
|
|
148
132
|
usage
|
|
149
133
|
.command('report')
|
|
150
134
|
.description('Internal: invoked by the Claude Code PostToolUse / UserPromptSubmit hook (reads stdin)')
|
|
@@ -159,12 +143,6 @@ hook
|
|
|
159
143
|
.command('uninstall')
|
|
160
144
|
.description('Remove the Prave-managed hook from ~/.claude/settings.json')
|
|
161
145
|
.action(usageHookUninstallCommand);
|
|
162
|
-
program
|
|
163
|
-
.command('deploy <skillname>')
|
|
164
|
-
.description('Deploy a Skill to every configured agent (Claude Code, Codex, Cursor, Gemini, Cline, Amp). Free plan deploys to Claude Code only; Pro and Max deploy across all six. The skill must already exist locally — use `prave install` first or point to a SKILL.md folder.')
|
|
165
|
-
.option('--agent <agent>', 'restrict deploy to a single agent (claude-code | codex | cursor | gemini | cline | amp)')
|
|
166
|
-
.option('--dry-run', 'log every destination path but write nothing — preview before committing')
|
|
167
|
-
.action(deployCommand);
|
|
168
146
|
program
|
|
169
147
|
.command('mcp-server')
|
|
170
148
|
.description('Run the Prave MCP server over stdio. Wire into Claude Desktop / Cursor MCP / Continue.dev via { "command": "npx", "args": ["-y", "@prave/cli", "mcp-server"] }. Exposes search, install, audit, my-skills, whatdoes as MCP tools.')
|
|
@@ -181,6 +159,10 @@ run
|
|
|
181
159
|
.command('deploy [path]')
|
|
182
160
|
.description('Bundle the current directory (or `path`), upload it to Prave, and open the browser wizard to pick the schedule + agent.')
|
|
183
161
|
.action((path) => runDeployCommand(path));
|
|
162
|
+
run
|
|
163
|
+
.command('update <slug> [path]')
|
|
164
|
+
.description("Re-upload the bundle for an existing run. Same flow as `deploy`, but the run's schedule, agent and env vars stay intact — only the project files swap.")
|
|
165
|
+
.action((slug, path) => runDeployCommand(path, { updateRunSlug: slug }));
|
|
184
166
|
run
|
|
185
167
|
.command('list')
|
|
186
168
|
.description('List your scheduled runs with next-fire time + last status.')
|
|
@@ -189,6 +171,10 @@ run
|
|
|
189
171
|
.command('logs <slug>')
|
|
190
172
|
.description('Print the latest execution log for a scheduled run.')
|
|
191
173
|
.action(runLogsCommand);
|
|
174
|
+
run
|
|
175
|
+
.command('trigger <slug>')
|
|
176
|
+
.description('Fire a one-shot execution outside the cron schedule. Refused with 409 if another execution is already in flight. Tail with `prave run logs <slug>` afterwards.')
|
|
177
|
+
.action(runTriggerCommand);
|
|
192
178
|
program
|
|
193
179
|
.command('mcp install')
|
|
194
180
|
.alias('mcp-install')
|
|
@@ -209,14 +195,12 @@ program
|
|
|
209
195
|
'',
|
|
210
196
|
'Discover & install',
|
|
211
197
|
' prave search <q> # public skill search',
|
|
212
|
-
' prave install <slug> [--no-deps] # install into ~/.claude/skills/',
|
|
198
|
+
' prave install <slug> [--no-deps] # install into ~/.claude/skills/ (multi-agent prompt included)',
|
|
213
199
|
' prave uninstall <slug> # remove a local skill',
|
|
214
200
|
' prave list [--remote] [--verbose] # what is installed (or remote)',
|
|
215
|
-
' prave export <slug> -o file.md # dump SKILL.md without installing',
|
|
216
201
|
'',
|
|
217
202
|
'Authoring & sync',
|
|
218
203
|
' prave import [--upload --public] # publish ~/.claude/skills/ to Prave',
|
|
219
|
-
' prave deploy <skill> [--agent x] # mirror to other agents',
|
|
220
204
|
' prave sync # pull every installed update',
|
|
221
205
|
' prave update [slug] [--dry-run] # diff + pull outdated skills',
|
|
222
206
|
' prave diff <slug> # local vs registry diff',
|
|
@@ -231,14 +215,14 @@ program
|
|
|
231
215
|
'Usage tracking (Pro+)',
|
|
232
216
|
' prave usage hook install # real-time PostToolUse hook',
|
|
233
217
|
' prave usage hook uninstall',
|
|
234
|
-
' prave usage scan [--since 7d] # transcript scanner',
|
|
235
|
-
' prave usage status # hook health + recent counts',
|
|
236
218
|
'',
|
|
237
219
|
'Settings',
|
|
238
220
|
' prave settings # configure agents + paths',
|
|
239
221
|
'',
|
|
240
222
|
'Runs (scheduled cron on Prave)',
|
|
241
223
|
' prave run deploy # bundle cwd, upload, open wizard',
|
|
224
|
+
' prave run update <slug> # re-upload bundle for an existing run',
|
|
225
|
+
' prave run trigger <slug> # fire one execution outside the cron',
|
|
242
226
|
' prave run list # your scheduled runs',
|
|
243
227
|
' prave run logs <slug> # tail latest execution log',
|
|
244
228
|
'',
|
package/dist/lib/api.js
CHANGED
|
@@ -15,39 +15,68 @@ export class ApiError extends Error {
|
|
|
15
15
|
* Supabase rate limiter.
|
|
16
16
|
*/
|
|
17
17
|
let refreshing = null;
|
|
18
|
+
/** One HTTP round-trip to /cli/refresh — returns null on any non-200. */
|
|
19
|
+
async function callRefresh(refresh_token) {
|
|
20
|
+
try {
|
|
21
|
+
const { statusCode, body } = await request(`${CONFIG.apiUrl}/api/v1/cli/refresh`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ refresh_token }),
|
|
25
|
+
});
|
|
26
|
+
const text = await body.text();
|
|
27
|
+
if (statusCode !== 200)
|
|
28
|
+
return null;
|
|
29
|
+
const payload = JSON.parse(text);
|
|
30
|
+
if (!payload.success || !payload.data)
|
|
31
|
+
return null;
|
|
32
|
+
return payload.data;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
18
38
|
async function refreshTokens() {
|
|
19
39
|
if (refreshing)
|
|
20
40
|
return refreshing;
|
|
21
41
|
refreshing = (async () => {
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
42
|
+
const initial = await loadCredentials();
|
|
43
|
+
if (!initial?.refresh_token)
|
|
24
44
|
return null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// First attempt: refresh with whatever's on disk right now.
|
|
46
|
+
let result = await callRefresh(initial.refresh_token);
|
|
47
|
+
let creds = initial;
|
|
48
|
+
// Supabase rotates the refresh_token on every successful refresh, so
|
|
49
|
+
// the old one is invalidated immediately. Two CLI processes can race
|
|
50
|
+
// here: the PostToolUse hook fires in the background while a
|
|
51
|
+
// foreground `prave whoami` (or anything else) is also alive. Each
|
|
52
|
+
// is its own Node process; the single-flight `refreshing` Promise
|
|
53
|
+
// only de-dupes within one process.
|
|
54
|
+
//
|
|
55
|
+
// When that race happens, the loser's stored refresh_token is now
|
|
56
|
+
// the already-used (and rejected) one. Re-read credentials.json
|
|
57
|
+
// once — if the file has moved on (another process wrote newer
|
|
58
|
+
// tokens), retry with the fresh refresh_token before giving up.
|
|
59
|
+
if (!result) {
|
|
60
|
+
const fresh = await loadCredentials();
|
|
61
|
+
if (fresh?.refresh_token &&
|
|
62
|
+
fresh.refresh_token !== initial.refresh_token) {
|
|
63
|
+
result = await callRefresh(fresh.refresh_token);
|
|
64
|
+
if (result)
|
|
65
|
+
creds = fresh;
|
|
66
|
+
}
|
|
47
67
|
}
|
|
48
|
-
|
|
68
|
+
if (!result)
|
|
49
69
|
return null;
|
|
50
|
-
|
|
70
|
+
const next = {
|
|
71
|
+
...creds,
|
|
72
|
+
access_token: result.access_token,
|
|
73
|
+
refresh_token: result.refresh_token ?? creds.refresh_token,
|
|
74
|
+
user_id: result.user_id,
|
|
75
|
+
// Persist the new expiry so the next call's proactive check works.
|
|
76
|
+
expires_at: result.expires_at ?? undefined,
|
|
77
|
+
};
|
|
78
|
+
await saveCredentials(next);
|
|
79
|
+
return next;
|
|
51
80
|
})();
|
|
52
81
|
try {
|
|
53
82
|
return await refreshing;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
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.4"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^20.12.7",
|
package/dist/commands/export.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { api } from '../lib/api.js';
|
|
3
|
-
import { loadCredentials } from '../lib/credentials.js';
|
|
4
|
-
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
5
|
-
import { log } from '../utils/logger.js';
|
|
6
|
-
/**
|
|
7
|
-
* `prave export <slug>` — print or save a Skill's SKILL.md to disk
|
|
8
|
-
* without touching ~/.claude/skills/. Useful for CI pipelines that want
|
|
9
|
-
* to bundle Skills into other repos.
|
|
10
|
-
*/
|
|
11
|
-
export async function exportCommand(slug, opts = {}) {
|
|
12
|
-
const session = await loadCredentials();
|
|
13
|
-
// Plan gate: export is part of the Pro+ authoring toolkit. Free can browse
|
|
14
|
-
// and install, not extract for republishing.
|
|
15
|
-
if (session) {
|
|
16
|
-
const me = await fetchMyPlan();
|
|
17
|
-
if (!me.limits.can_authoring_public && !me.limits.can_authoring_private) {
|
|
18
|
-
log.warn('Export requires the Pro plan or higher.');
|
|
19
|
-
log.dim(formatUpgradeHint('explorer'));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const { data: skill } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, Boolean(session));
|
|
24
|
-
const content = skill.content ?? '';
|
|
25
|
-
if (!content.trim()) {
|
|
26
|
-
log.warn(`${slug} has no content`);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (opts.out) {
|
|
30
|
-
await writeFile(opts.out, content, 'utf8');
|
|
31
|
-
log.success(`Wrote ${opts.out} (${content.length} bytes)`);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
process.stdout.write(content);
|
|
35
|
-
}
|