@prave/cli 0.1.0 → 0.2.0
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/export.js +11 -0
- package/dist/commands/import.js +48 -4
- package/dist/commands/sync.js +12 -0
- package/dist/lib/plan.js +31 -0
- package/package.json +2 -2
package/dist/commands/export.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
2
|
import { api } from '../lib/api.js';
|
|
3
3
|
import { loadCredentials } from '../lib/credentials.js';
|
|
4
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
/**
|
|
6
7
|
* `prave export <slug>` — print or save a Skill's SKILL.md to disk
|
|
@@ -9,6 +10,16 @@ import { log } from '../utils/logger.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export async function exportCommand(slug, opts = {}) {
|
|
11
12
|
const session = await loadCredentials();
|
|
13
|
+
// Plan gate: Free can't export. Explorer caps at 50/mo (server-enforced
|
|
14
|
+
// via per-user counter once that lands; CLI-side we only flag the gate).
|
|
15
|
+
if (session) {
|
|
16
|
+
const me = await fetchMyPlan();
|
|
17
|
+
if (me.limits.cli_exports_monthly === 0) {
|
|
18
|
+
log.warn('Export requires the Explorer plan or higher.');
|
|
19
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
const { data: skill } = await api.get(`/api/v1/skills/${encodeURIComponent(slug)}`, Boolean(session));
|
|
13
24
|
const content = skill.content ?? '';
|
|
14
25
|
if (!content.trim()) {
|
package/dist/commands/import.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { api } from '../lib/api.js';
|
|
6
6
|
import { CONFIG } from '../lib/config.js';
|
|
7
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
7
8
|
import { log } from '../utils/logger.js';
|
|
8
9
|
/**
|
|
9
10
|
* Scans CONFIG.skillsDir for SKILL.md files. Without --upload, only prints
|
|
@@ -42,10 +43,27 @@ export async function importCommand(opts) {
|
|
|
42
43
|
log.dim('\nRe-run with --upload to push these to your Prave account.');
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
46
|
+
// Plan gate: clamp the queue to the caller's import / private quota so
|
|
47
|
+
// we don't waste 70 round-trips against a Free account that maxes at 10.
|
|
48
|
+
const me = await fetchMyPlan();
|
|
45
49
|
const visibility = opts.private ? 'private' : 'public';
|
|
46
|
-
|
|
50
|
+
if (visibility === 'private' && !me.limits.can_private_skills) {
|
|
51
|
+
log.warn('Private imports require the Explorer plan.');
|
|
52
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let queue = skills;
|
|
56
|
+
if (me.limits.cli_max_imports !== null && queue.length > me.limits.cli_max_imports) {
|
|
57
|
+
log.warn(`Your ${me.plan} plan caps imports at ${me.limits.cli_max_imports}. Trimming queue from ${queue.length} → ${me.limits.cli_max_imports}.`);
|
|
58
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
59
|
+
queue = queue.slice(0, me.limits.cli_max_imports);
|
|
60
|
+
}
|
|
61
|
+
const uploadSpinner = ora(`Uploading ${queue.length} skills as ${visibility}…`).start();
|
|
47
62
|
let ok = 0;
|
|
48
|
-
|
|
63
|
+
let skipped = 0;
|
|
64
|
+
let failed = 0;
|
|
65
|
+
let gated = 0;
|
|
66
|
+
for (const s of queue) {
|
|
49
67
|
try {
|
|
50
68
|
await api.post('/api/v1/skills', {
|
|
51
69
|
name: s.slug,
|
|
@@ -57,8 +75,34 @@ export async function importCommand(opts) {
|
|
|
57
75
|
ok += 1;
|
|
58
76
|
}
|
|
59
77
|
catch (err) {
|
|
60
|
-
|
|
78
|
+
const msg = err.message ?? 'unknown error';
|
|
79
|
+
// The API returns 409 when an identical-content public skill already
|
|
80
|
+
// exists. That isn't a real failure on import — flag it as skipped so
|
|
81
|
+
// the user can keep going.
|
|
82
|
+
if (/already exists/i.test(msg) || /409/.test(msg)) {
|
|
83
|
+
skipped += 1;
|
|
84
|
+
log.dim(` ↷ skipped ${s.slug} — already on Prave`);
|
|
85
|
+
}
|
|
86
|
+
else if (/402/.test(msg) || /upgrade/i.test(msg) || /caps/i.test(msg)) {
|
|
87
|
+
// Plan limit hit on the server (e.g. max public skills reached).
|
|
88
|
+
gated += 1;
|
|
89
|
+
uploadSpinner.warn(`Quota: ${s.slug} — ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
failed += 1;
|
|
93
|
+
uploadSpinner.warn(`Failed: ${s.slug} — ${msg}`);
|
|
94
|
+
}
|
|
61
95
|
}
|
|
62
96
|
}
|
|
63
|
-
|
|
97
|
+
const tail = [
|
|
98
|
+
`Uploaded ${ok}/${queue.length}`,
|
|
99
|
+
skipped ? `${skipped} skipped (dupes)` : null,
|
|
100
|
+
gated ? `${gated} blocked by plan` : null,
|
|
101
|
+
failed ? `${failed} failed` : null,
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(' · ');
|
|
105
|
+
uploadSpinner.succeed(tail);
|
|
106
|
+
if (gated > 0)
|
|
107
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
64
108
|
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -3,6 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { CONFIG } from '../lib/config.js';
|
|
6
|
+
import { loadCredentials } from '../lib/credentials.js';
|
|
7
|
+
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
6
8
|
import { log } from '../utils/logger.js';
|
|
7
9
|
import { installCommand } from './install.js';
|
|
8
10
|
/**
|
|
@@ -12,6 +14,16 @@ import { installCommand } from './install.js';
|
|
|
12
14
|
* resolved on the original install).
|
|
13
15
|
*/
|
|
14
16
|
export async function syncCommand() {
|
|
17
|
+
// Plan gate: sync requires Explorer or higher.
|
|
18
|
+
const session = await loadCredentials();
|
|
19
|
+
if (session) {
|
|
20
|
+
const me = await fetchMyPlan();
|
|
21
|
+
if (!me.limits.can_cli_sync) {
|
|
22
|
+
log.warn('Sync requires the Explorer plan or higher.');
|
|
23
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
15
27
|
const spinner = ora('Scanning local Skills…').start();
|
|
16
28
|
let entries = [];
|
|
17
29
|
try {
|
package/dist/lib/plan.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PLAN_LIMITS } from '@prave/shared';
|
|
2
|
+
import { api } from './api.js';
|
|
3
|
+
let cache = null;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the caller's plan + limits via /me/plan. Cached for the
|
|
6
|
+
* lifetime of the CLI invocation since plan changes mid-command are not
|
|
7
|
+
* a real concern. On any error (auth missing, network) we fall back to
|
|
8
|
+
* 'free' so commands still gracefully gate.
|
|
9
|
+
*/
|
|
10
|
+
export const fetchMyPlan = async () => {
|
|
11
|
+
if (cache)
|
|
12
|
+
return cache;
|
|
13
|
+
try {
|
|
14
|
+
const { data } = await api.get('/api/v1/me/plan', true);
|
|
15
|
+
cache = data;
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
const fallback = {
|
|
20
|
+
plan: 'free',
|
|
21
|
+
limits: PLAN_LIMITS.free,
|
|
22
|
+
subscription_status: null,
|
|
23
|
+
current_period_end: null,
|
|
24
|
+
};
|
|
25
|
+
cache = fallback;
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const formatUpgradeHint = (required) => required === 'creator'
|
|
30
|
+
? 'Upgrade to Creator at https://prave.app/dashboard/settings'
|
|
31
|
+
: 'Upgrade to Explorer (€4/mo) at https://prave.app/dashboard/settings';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Prave CLI — import, export, install, sync Claude Skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"open": "^10.1.0",
|
|
17
17
|
"ora": "^8.0.1",
|
|
18
18
|
"undici": "^6.18.0",
|
|
19
|
-
"@prave/shared": "0.
|
|
19
|
+
"@prave/shared": "0.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.12.7",
|