@prave/cli 0.4.10 → 0.5.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/conflicts.js +4 -0
- package/dist/commands/deploy.js +4 -0
- package/dist/commands/import.js +24 -2
- package/dist/commands/install.js +14 -7
- package/dist/commands/optimize.js +4 -0
- package/dist/commands/overview.js +4 -0
- package/dist/commands/settings.js +4 -0
- package/dist/commands/sync.js +43 -12
- package/dist/commands/update.js +165 -0
- package/dist/commands/whoami.js +31 -3
- package/dist/index.js +10 -2
- package/dist/lib/api.js +54 -2
- package/dist/lib/credentials.js +21 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
function describe(c) {
|
|
6
7
|
const a = c.skill_a_name ?? c.skill_a_id;
|
|
@@ -17,6 +18,9 @@ function describe(c) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
export async function conflictsCommand(opts = {}) {
|
|
21
|
+
const _session = await requireAuth("prave conflicts");
|
|
22
|
+
if (!_session)
|
|
23
|
+
return;
|
|
20
24
|
const spinner = ora('Detecting conflicts…').start();
|
|
21
25
|
try {
|
|
22
26
|
const { data: conflicts } = await api.get('/api/v1/intelligence/conflicts?refresh=true', true);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { AGENT_REGISTRY } from '@prave/shared';
|
|
7
7
|
import { api, ApiError } from '../lib/api.js';
|
|
8
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
8
9
|
import { CONFIG } from '../lib/config.js';
|
|
9
10
|
import { log } from '../utils/logger.js';
|
|
10
11
|
function detectOsKey(detected) {
|
|
@@ -68,6 +69,9 @@ function buildDestPath(agent, basePath, os, slug) {
|
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
export async function deployCommand(skillName, opts = {}) {
|
|
72
|
+
const session = await requireAuth('prave deploy');
|
|
73
|
+
if (!session)
|
|
74
|
+
return;
|
|
71
75
|
const start = Date.now();
|
|
72
76
|
let settings;
|
|
73
77
|
try {
|
package/dist/commands/import.js
CHANGED
|
@@ -4,14 +4,27 @@ 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 { loadCredentials } from '../lib/credentials.js';
|
|
7
|
+
import { loadCredentials, requireAuth } from '../lib/credentials.js';
|
|
8
8
|
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
9
9
|
import { log } from '../utils/logger.js';
|
|
10
10
|
/**
|
|
11
11
|
* Scans CONFIG.skillsDir for SKILL.md files. Without --upload, only prints
|
|
12
12
|
* the overview — no network call, no consent required.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Visibility is mandatory on uploads — no silent defaults. Users must pass
|
|
16
|
+
* either `--public` or `--private`. This avoids the embarrassment of
|
|
17
|
+
* accidentally publishing private notes to the registry.
|
|
18
|
+
*/
|
|
14
19
|
export async function importCommand(opts) {
|
|
20
|
+
// `prave import` (without --upload) only inspects local files and posts
|
|
21
|
+
// to /intelligence/analyze if logged in — uploads, however, must always
|
|
22
|
+
// be tracked, so we gate on auth whenever --upload is set.
|
|
23
|
+
if (opts.upload) {
|
|
24
|
+
const session = await requireAuth('prave import --upload');
|
|
25
|
+
if (!session)
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
15
28
|
const spinner = ora(`Scanning ${CONFIG.skillsDir}…`).start();
|
|
16
29
|
let entries = [];
|
|
17
30
|
try {
|
|
@@ -56,10 +69,19 @@ export async function importCommand(opts) {
|
|
|
56
69
|
}
|
|
57
70
|
return;
|
|
58
71
|
}
|
|
72
|
+
// Visibility is required on every upload — no silent default.
|
|
73
|
+
if (opts.public === opts.private) {
|
|
74
|
+
log.warn('Specify visibility: --public or --private (one is required on upload).');
|
|
75
|
+
log.dim('Examples:');
|
|
76
|
+
log.dim(' prave import --upload --private # only you can see them');
|
|
77
|
+
log.dim(' prave import --upload --public # listed in the public registry');
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const visibility = opts.private ? 'private' : 'public';
|
|
59
82
|
// Plan gate: clamp the queue to the caller's import / private quota so
|
|
60
83
|
// we don't waste 70 round-trips against a Free account that maxes at 10.
|
|
61
84
|
const me = await fetchMyPlan();
|
|
62
|
-
const visibility = opts.private ? 'private' : 'public';
|
|
63
85
|
if (visibility === 'private' && !me.limits.can_private_skills) {
|
|
64
86
|
log.warn('Private imports require the Explorer plan.');
|
|
65
87
|
log.dim(formatUpgradeHint('explorer'));
|
package/dist/commands/install.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { api, ApiError } from '../lib/api.js';
|
|
7
7
|
import { CONFIG } from '../lib/config.js';
|
|
8
|
-
import { loadCredentials } from '../lib/credentials.js';
|
|
8
|
+
import { loadCredentials, requireAuth } from '../lib/credentials.js';
|
|
9
9
|
import { log } from '../utils/logger.js';
|
|
10
10
|
/**
|
|
11
11
|
* `prave install <slug>` — pulls SKILL.md to ~/.claude/skills/<slug>/.
|
|
@@ -18,15 +18,21 @@ import { log } from '../utils/logger.js';
|
|
|
18
18
|
* instead of letting the API error bubble.
|
|
19
19
|
*/
|
|
20
20
|
export async function installCommand(slug, opts = {}) {
|
|
21
|
+
// Auth gate — installs MUST be tracked, otherwise we can't bump counts,
|
|
22
|
+
// serve recommendations, or surface "updates available" later. Block here
|
|
23
|
+
// before any registry hit so the user gets a clear hint instead of a
|
|
24
|
+
// silently-untracked install.
|
|
25
|
+
const session = await requireAuth('prave install');
|
|
26
|
+
if (!session)
|
|
27
|
+
return;
|
|
21
28
|
const spinner = ora(`Resolving ${slug}…`).start();
|
|
22
29
|
let installedSlugs = [];
|
|
23
30
|
try {
|
|
24
31
|
const slugs = opts.noDeps ? [slug] : await resolveOrder(slug);
|
|
25
32
|
spinner.text = `Installing ${slugs.length} skill${slugs.length === 1 ? '' : 's'}…`;
|
|
26
|
-
const session = await loadCredentials();
|
|
27
33
|
for (const s of slugs) {
|
|
28
34
|
spinner.text = `↓ ${s}`;
|
|
29
|
-
await pullOne(s, { hasSession:
|
|
35
|
+
await pullOne(s, { hasSession: true, force: Boolean(opts.force) });
|
|
30
36
|
}
|
|
31
37
|
installedSlugs = slugs;
|
|
32
38
|
spinner.succeed(`Installed ${slugs.length} skill${slugs.length === 1 ? '' : 's'} → ${CONFIG.skillsDir}`);
|
|
@@ -39,9 +45,6 @@ export async function installCommand(slug, opts = {}) {
|
|
|
39
45
|
process.exitCode = 1;
|
|
40
46
|
return;
|
|
41
47
|
}
|
|
42
|
-
const session = await loadCredentials();
|
|
43
|
-
if (!session)
|
|
44
|
-
return;
|
|
45
48
|
// Best-effort analyze pass on the freshly written files.
|
|
46
49
|
for (const s of installedSlugs) {
|
|
47
50
|
const path = join(CONFIG.skillsDir, s, 'SKILL.md');
|
|
@@ -55,7 +58,11 @@ export async function installCommand(slug, opts = {}) {
|
|
|
55
58
|
/* file unreadable — skip */
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
|
-
// Offer to deploy to all configured agents
|
|
61
|
+
// Offer to deploy to all configured agents — unless the caller already
|
|
62
|
+
// answered the question (e.g. `prave sync` collects the answer once for
|
|
63
|
+
// the whole batch and threads it through `skipDeployPrompt`).
|
|
64
|
+
if (opts.skipDeployPrompt)
|
|
65
|
+
return;
|
|
59
66
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
60
67
|
try {
|
|
61
68
|
const ans = (await rl.question('\nDeploy to all configured agents? [y/N] ')).trim().toLowerCase();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
function formatTokens(n) {
|
|
6
7
|
if (n < 1000)
|
|
@@ -11,6 +12,9 @@ function nameOf(s) {
|
|
|
11
12
|
return s.name ?? s.file_path;
|
|
12
13
|
}
|
|
13
14
|
export async function optimizeCommand(opts = {}) {
|
|
15
|
+
const _session = await requireAuth("prave optimize");
|
|
16
|
+
if (!_session)
|
|
17
|
+
return;
|
|
14
18
|
const spinner = ora('Analyzing your skill set…').start();
|
|
15
19
|
try {
|
|
16
20
|
const { data } = await api.get('/api/v1/intelligence/optimize', true);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { api, ApiError } from '../lib/api.js';
|
|
4
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
function formatThousands(n) {
|
|
6
7
|
if (n < 1000)
|
|
@@ -13,6 +14,9 @@ function pad(s, width) {
|
|
|
13
14
|
return s + ' '.repeat(width - s.length);
|
|
14
15
|
}
|
|
15
16
|
export async function overviewCommand(opts = {}) {
|
|
17
|
+
const _session = await requireAuth("prave overview");
|
|
18
|
+
if (!_session)
|
|
19
|
+
return;
|
|
16
20
|
if (opts.json) {
|
|
17
21
|
try {
|
|
18
22
|
const { data } = await api.get('/api/v1/intelligence/overview', true);
|
|
@@ -3,6 +3,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { AGENT_REGISTRY, AGENT_TYPES } from '@prave/shared';
|
|
5
5
|
import { api, ApiError } from '../lib/api.js';
|
|
6
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
6
7
|
import { CONFIG } from '../lib/config.js';
|
|
7
8
|
import { log } from '../utils/logger.js';
|
|
8
9
|
function detectOs() {
|
|
@@ -130,6 +131,9 @@ async function showAccount() {
|
|
|
130
131
|
log.dim('Run `prave whoami` for full identity, or `prave logout` to sign out.');
|
|
131
132
|
}
|
|
132
133
|
export async function settingsCommand() {
|
|
134
|
+
const _session = await requireAuth("prave settings");
|
|
135
|
+
if (!_session)
|
|
136
|
+
return;
|
|
133
137
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
134
138
|
try {
|
|
135
139
|
let current;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import ora from 'ora';
|
|
5
6
|
import { CONFIG } from '../lib/config.js';
|
|
6
|
-
import {
|
|
7
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
7
8
|
import { fetchMyPlan, formatUpgradeHint } from '../lib/plan.js';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
9
10
|
import { installCommand } from './install.js';
|
|
10
11
|
/**
|
|
11
12
|
* `prave sync` — re-pulls every locally installed Skill from the
|
|
12
13
|
* registry. Picks up SKILL.md edits without the user having to remember
|
|
13
|
-
* each slug.
|
|
14
|
-
*
|
|
14
|
+
* each slug.
|
|
15
|
+
*
|
|
16
|
+
* The deploy-to-all-agents question is asked ONCE up-front for the entire
|
|
17
|
+
* queue (regression fix: it used to fire per Skill, forcing the user to
|
|
18
|
+
* mash `y` 30 times). The answer is then threaded into every
|
|
19
|
+
* `installCommand` invocation via `skipDeployPrompt: true` and we run a
|
|
20
|
+
* single batched deploy at the end.
|
|
15
21
|
*/
|
|
16
22
|
export async function syncCommand() {
|
|
23
|
+
// Auth gate — sync mutates the install ledger and intelligence cache.
|
|
24
|
+
const session = await requireAuth('prave sync');
|
|
25
|
+
if (!session)
|
|
26
|
+
return;
|
|
17
27
|
// Plan gate: sync requires Explorer or higher.
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
log.dim(formatUpgradeHint('explorer'));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
28
|
+
const me = await fetchMyPlan();
|
|
29
|
+
if (!me.limits.can_cli_sync) {
|
|
30
|
+
log.warn('Sync requires the Explorer plan or higher.');
|
|
31
|
+
log.dim(formatUpgradeHint('explorer'));
|
|
32
|
+
return;
|
|
26
33
|
}
|
|
27
34
|
const spinner = ora('Scanning local Skills…').start();
|
|
28
35
|
let entries = [];
|
|
@@ -44,11 +51,23 @@ export async function syncCommand() {
|
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
spinner.succeed(`Found ${slugs.length} installed Skill${slugs.length === 1 ? '' : 's'}.`);
|
|
54
|
+
// Ask the deploy question ONCE for the whole batch.
|
|
55
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
let deployAfter = false;
|
|
57
|
+
try {
|
|
58
|
+
const ans = (await rl.question(`\nAfter syncing, deploy all ${slugs.length} Skills to your configured agents? [y/N] `))
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
deployAfter = ans === 'y' || ans === 'yes';
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
rl.close();
|
|
65
|
+
}
|
|
47
66
|
let updated = 0;
|
|
48
67
|
let failed = 0;
|
|
49
68
|
for (const slug of slugs) {
|
|
50
69
|
try {
|
|
51
|
-
await installCommand(slug, { noDeps: true });
|
|
70
|
+
await installCommand(slug, { noDeps: true, skipDeployPrompt: true });
|
|
52
71
|
updated++;
|
|
53
72
|
}
|
|
54
73
|
catch {
|
|
@@ -57,4 +76,16 @@ export async function syncCommand() {
|
|
|
57
76
|
}
|
|
58
77
|
}
|
|
59
78
|
log.dim(`\nSynced ${updated} · failed ${failed}`);
|
|
79
|
+
if (deployAfter && updated > 0) {
|
|
80
|
+
const { deployCommand } = await import('./deploy.js');
|
|
81
|
+
log.info(`\nDeploying ${updated} Skills to configured agents…`);
|
|
82
|
+
for (const slug of slugs) {
|
|
83
|
+
try {
|
|
84
|
+
await deployCommand(slug, {});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
log.warn(` ✗ deploy ${slug} — ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
60
91
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { api } from '../lib/api.js';
|
|
7
|
+
import { CONFIG } from '../lib/config.js';
|
|
8
|
+
import { requireAuth } from '../lib/credentials.js';
|
|
9
|
+
import { log } from '../utils/logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* `prave update [<slug>]` — diff every CLI-installed Skill against its
|
|
12
|
+
* current Prave version and pull the ones that are outdated.
|
|
13
|
+
*
|
|
14
|
+
* • No slug → walks the install ledger, compares each install's
|
|
15
|
+
* `installed_at` vs the Skill's `updated_at`, and updates the stale
|
|
16
|
+
* ones in-place under ~/.claude/skills/<slug>/SKILL.md.
|
|
17
|
+
* • Slug given → updates exactly that Skill (force-pulls latest, even if
|
|
18
|
+
* not stale — useful when the local file was edited and needs reset).
|
|
19
|
+
*
|
|
20
|
+
* Prints a short "audit-trail" line per Skill so the operator can see what
|
|
21
|
+
* changed: `markdown-pro v3 → v5 (last pulled 12 Apr · published 21 Apr)`.
|
|
22
|
+
*/
|
|
23
|
+
export async function updateCommand(slug, opts = {}) {
|
|
24
|
+
const session = await requireAuth('prave update');
|
|
25
|
+
if (!session)
|
|
26
|
+
return;
|
|
27
|
+
const spinner = ora(slug ? `Resolving ${slug}…` : 'Checking your installed skills…').start();
|
|
28
|
+
let installs;
|
|
29
|
+
try {
|
|
30
|
+
const { data } = await api.get('/api/v1/me/installs', true);
|
|
31
|
+
installs = data;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
spinner.fail(`Couldn't fetch installs — ${err.message}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const targets = slug ? installs.filter((r) => r.skill?.slug === slug) : installs;
|
|
39
|
+
if (targets.length === 0) {
|
|
40
|
+
spinner.info(slug ? `${slug} is not in your install ledger.` : 'No installs on this account yet.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Fetch the current Skill for each target in parallel.
|
|
44
|
+
spinner.text = `Comparing ${targets.length} skill${targets.length === 1 ? '' : 's'}…`;
|
|
45
|
+
const candidates = [];
|
|
46
|
+
const upToDate = [];
|
|
47
|
+
const errors = [];
|
|
48
|
+
await Promise.all(targets.map(async (row) => {
|
|
49
|
+
const targetSlug = row.skill?.slug;
|
|
50
|
+
if (!targetSlug)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
const { data: skill } = await api.get(`/api/v1/skills/${targetSlug}`, true);
|
|
54
|
+
const isStale = isOutdated(row, skill, Boolean(slug));
|
|
55
|
+
if (!isStale) {
|
|
56
|
+
upToDate.push(targetSlug);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
candidates.push({
|
|
60
|
+
slug: targetSlug,
|
|
61
|
+
installedAt: row.installed_at,
|
|
62
|
+
serverUpdatedAt: skill.updated_at,
|
|
63
|
+
installedVersion: row.version ?? null,
|
|
64
|
+
latestVersion: typeof skill.current_version === 'number'
|
|
65
|
+
? skill.current_version ?? null
|
|
66
|
+
: null,
|
|
67
|
+
remoteContent: skill.content ?? '',
|
|
68
|
+
localPath: join(CONFIG.skillsDir, targetSlug, 'SKILL.md'),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
errors.push(`${targetSlug}: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
if (candidates.length === 0) {
|
|
76
|
+
if (errors.length > 0) {
|
|
77
|
+
spinner.warn(`No updates available. ${errors.length} skill(s) couldn't be checked:`);
|
|
78
|
+
for (const e of errors)
|
|
79
|
+
log.dim(` ✗ ${e}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
spinner.succeed(upToDate.length === 1
|
|
83
|
+
? `${upToDate[0]} is up to date.`
|
|
84
|
+
: `All ${upToDate.length} installed skills are up to date.`);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
spinner.succeed(`${candidates.length} skill${candidates.length === 1 ? '' : 's'} can be updated.`);
|
|
89
|
+
// Audit-trail print — same format as a tiny commit log.
|
|
90
|
+
for (const c of candidates) {
|
|
91
|
+
const versionLine = c.installedVersion !== null && c.latestVersion !== null
|
|
92
|
+
? `${chalk.dim('v')}${c.installedVersion} → ${chalk.bold('v' + c.latestVersion)}`
|
|
93
|
+
: chalk.dim('content-hash diff');
|
|
94
|
+
console.log(` ${chalk.cyan('•')} ${chalk.bold(c.slug)} ${versionLine} ${chalk.dim(`(local ${formatDate(c.installedAt)} → registry ${formatDate(c.serverUpdatedAt)})`)}`);
|
|
95
|
+
}
|
|
96
|
+
if (opts.dryRun) {
|
|
97
|
+
log.dim(`\nDry run — nothing written. Re-run without --dry-run to apply.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!opts.yes) {
|
|
101
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
102
|
+
try {
|
|
103
|
+
const ans = (await rl.question(`\nPull ${candidates.length} update${candidates.length === 1 ? '' : 's'}? [Y/n] `))
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase();
|
|
106
|
+
if (ans && ans !== 'y' && ans !== 'yes') {
|
|
107
|
+
log.dim('Aborted.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
rl.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const writeSpinner = ora(`Writing ${candidates.length} update${candidates.length === 1 ? '' : 's'}…`).start();
|
|
116
|
+
let written = 0;
|
|
117
|
+
for (const c of candidates) {
|
|
118
|
+
try {
|
|
119
|
+
await mkdir(join(CONFIG.skillsDir, c.slug), { recursive: true });
|
|
120
|
+
await writeFile(c.localPath, c.remoteContent, 'utf8');
|
|
121
|
+
// Re-record the install row so installed_at/version reflect the pull.
|
|
122
|
+
await api
|
|
123
|
+
.post(`/api/v1/skills/${encodeURIComponent(c.slug)}/install`, { version: c.latestVersion ?? null }, true)
|
|
124
|
+
.catch(() => { });
|
|
125
|
+
written += 1;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
writeSpinner.warn(`${c.slug}: ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
writeSpinner.succeed(`Updated ${written}/${candidates.length}${written === candidates.length ? '' : ` (${candidates.length - written} failed)`}.`);
|
|
132
|
+
if (errors.length > 0) {
|
|
133
|
+
log.dim(`\n${errors.length} skill(s) couldn't be checked earlier:`);
|
|
134
|
+
for (const e of errors)
|
|
135
|
+
log.dim(` ✗ ${e}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* "Stale" = the registry's `updated_at` is strictly newer than the
|
|
140
|
+
* install ledger's `installed_at`. For an explicit `prave update <slug>`
|
|
141
|
+
* we always treat the row as stale — the user is asking for a forced pull,
|
|
142
|
+
* which is also the right behaviour after a local edit gone wrong.
|
|
143
|
+
*/
|
|
144
|
+
function isOutdated(row, skill, force) {
|
|
145
|
+
if (force)
|
|
146
|
+
return true;
|
|
147
|
+
const installedTs = new Date(row.installed_at).getTime();
|
|
148
|
+
const updatedTs = new Date(skill.updated_at).getTime();
|
|
149
|
+
return Number.isFinite(installedTs) && Number.isFinite(updatedTs) && updatedTs > installedTs;
|
|
150
|
+
}
|
|
151
|
+
function formatDate(iso) {
|
|
152
|
+
const d = new Date(iso);
|
|
153
|
+
if (Number.isNaN(d.getTime()))
|
|
154
|
+
return iso;
|
|
155
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
156
|
+
}
|
|
157
|
+
/** Read the local SKILL.md content if it exists — for the `--from-disk` flag (Phase 2). */
|
|
158
|
+
export async function readLocalSkillContent(slug) {
|
|
159
|
+
try {
|
|
160
|
+
return await readFile(join(CONFIG.skillsDir, slug, 'SKILL.md'), 'utf8');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { ApiError, api } from '../lib/api.js';
|
|
1
2
|
import { loadCredentials } from '../lib/credentials.js';
|
|
2
3
|
import { log } from '../utils/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* `prave whoami` — quick check that the local credentials still authenticate
|
|
6
|
+
* against the API. Hits `GET /api/v1/me` so the displayed name is always the
|
|
7
|
+
* server-side truth (and any stale local data gets refreshed implicitly via
|
|
8
|
+
* the auto-refresh path on api.get).
|
|
9
|
+
*
|
|
10
|
+
* If we can't reach the API (offline, server down) we fall back to the
|
|
11
|
+
* locally cached email so the command stays useful in airplane mode.
|
|
12
|
+
*/
|
|
3
13
|
export async function whoamiCommand() {
|
|
4
14
|
const creds = await loadCredentials();
|
|
5
15
|
if (!creds) {
|
|
@@ -7,7 +17,25 @@ export async function whoamiCommand() {
|
|
|
7
17
|
process.exitCode = 1;
|
|
8
18
|
return;
|
|
9
19
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
try {
|
|
21
|
+
const { data } = await api.get('/api/v1/me', true);
|
|
22
|
+
const handle = data.username || data.display_name || data.email || creds.email || 'unknown';
|
|
23
|
+
log.kv('user', handle);
|
|
24
|
+
if (data.email)
|
|
25
|
+
log.kv('email', data.email);
|
|
26
|
+
log.kv('plan', data.plan);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
30
|
+
log.warn('Session expired. Run `prave login` again.');
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Offline or transient API error — fall back to local creds so the
|
|
35
|
+
// command still does *something* useful instead of crashing.
|
|
36
|
+
if (creds.email)
|
|
37
|
+
log.kv('user', creds.email);
|
|
38
|
+
else
|
|
39
|
+
log.kv('user', creds.user_id);
|
|
40
|
+
}
|
|
13
41
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { searchCommand } from './commands/search.js';
|
|
|
19
19
|
import { settingsCommand } from './commands/settings.js';
|
|
20
20
|
import { syncCommand } from './commands/sync.js';
|
|
21
21
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
22
|
+
import { updateCommand } from './commands/update.js';
|
|
22
23
|
import { whatdoesCommand } from './commands/whatdoes.js';
|
|
23
24
|
import { whoamiCommand } from './commands/whoami.js';
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -33,8 +34,9 @@ program.command('whoami').description('Show the signed-in user').action(whoamiCo
|
|
|
33
34
|
program
|
|
34
35
|
.command('import')
|
|
35
36
|
.description('Scan ~/.claude/skills/ — optionally upload to Prave')
|
|
36
|
-
.option('--upload', 'upload scanned skills')
|
|
37
|
-
.option('--
|
|
37
|
+
.option('--upload', 'upload scanned skills (requires --public or --private)')
|
|
38
|
+
.option('--public', 'upload as public (listed in the registry)')
|
|
39
|
+
.option('--private', 'upload as private (only you can see them)')
|
|
38
40
|
.action(importCommand);
|
|
39
41
|
program
|
|
40
42
|
.command('install <slug>')
|
|
@@ -50,6 +52,12 @@ program
|
|
|
50
52
|
.command('sync')
|
|
51
53
|
.description('Pull updates for every locally installed Skill')
|
|
52
54
|
.action(syncCommand);
|
|
55
|
+
program
|
|
56
|
+
.command('update [slug]')
|
|
57
|
+
.description('Diff installed Skills against the registry and pull anything outdated')
|
|
58
|
+
.option('--dry-run', "preview what would change, don't write")
|
|
59
|
+
.option('--yes', 'skip confirmation prompt')
|
|
60
|
+
.action((slug, opts) => updateCommand(slug, opts));
|
|
53
61
|
program
|
|
54
62
|
.command('list')
|
|
55
63
|
.description('List installed Skills (default) or remote ones')
|
package/dist/lib/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { request } from 'undici';
|
|
2
2
|
import { CONFIG } from './config.js';
|
|
3
|
-
import { loadCredentials } from './credentials.js';
|
|
3
|
+
import { loadCredentials, saveCredentials } from './credentials.js';
|
|
4
4
|
export class ApiError extends Error {
|
|
5
5
|
status;
|
|
6
6
|
constructor(message, status) {
|
|
@@ -9,7 +9,52 @@ export class ApiError extends Error {
|
|
|
9
9
|
this.name = 'ApiError';
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Single-flight refresh — multiple parallel requests all hitting 401 share
|
|
14
|
+
* one refresh round-trip instead of stampeding the API and burning the
|
|
15
|
+
* Supabase rate limiter.
|
|
16
|
+
*/
|
|
17
|
+
let refreshing = null;
|
|
18
|
+
async function refreshTokens() {
|
|
19
|
+
if (refreshing)
|
|
20
|
+
return refreshing;
|
|
21
|
+
refreshing = (async () => {
|
|
22
|
+
const creds = await loadCredentials();
|
|
23
|
+
if (!creds?.refresh_token)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const { statusCode, body } = await request(`${CONFIG.apiUrl}/api/v1/cli/refresh`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ refresh_token: creds.refresh_token }),
|
|
30
|
+
});
|
|
31
|
+
const text = await body.text();
|
|
32
|
+
if (statusCode !== 200)
|
|
33
|
+
return null;
|
|
34
|
+
const payload = JSON.parse(text);
|
|
35
|
+
if (!payload.success || !payload.data)
|
|
36
|
+
return null;
|
|
37
|
+
const next = {
|
|
38
|
+
...creds,
|
|
39
|
+
access_token: payload.data.access_token,
|
|
40
|
+
refresh_token: payload.data.refresh_token ?? creds.refresh_token,
|
|
41
|
+
user_id: payload.data.user_id,
|
|
42
|
+
};
|
|
43
|
+
await saveCredentials(next);
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
try {
|
|
51
|
+
return await refreshing;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
refreshing = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function call(method, path, body, withAuth = false, attempt = 0) {
|
|
13
58
|
const headers = { 'Content-Type': 'application/json' };
|
|
14
59
|
if (withAuth) {
|
|
15
60
|
const creds = await loadCredentials();
|
|
@@ -26,6 +71,13 @@ async function call(method, path, body, withAuth = false) {
|
|
|
26
71
|
if (statusCode === 204)
|
|
27
72
|
return { data: undefined, status: statusCode };
|
|
28
73
|
const payload = text ? JSON.parse(text) : { success: true, data: null, error: null };
|
|
74
|
+
// 401 with a refresh token on file → swap the access token and retry once.
|
|
75
|
+
// Any other status, or a second 401, falls through to the normal error path.
|
|
76
|
+
if (statusCode === 401 && withAuth && attempt === 0) {
|
|
77
|
+
const refreshed = await refreshTokens();
|
|
78
|
+
if (refreshed)
|
|
79
|
+
return call(method, path, body, withAuth, attempt + 1);
|
|
80
|
+
}
|
|
29
81
|
if (statusCode >= 400 || payload.success === false) {
|
|
30
82
|
throw new ApiError(payload.error ?? `HTTP ${statusCode}`, statusCode);
|
|
31
83
|
}
|
package/dist/lib/credentials.js
CHANGED
|
@@ -23,3 +23,24 @@ export async function clearCredentials() {
|
|
|
23
23
|
/* already gone */
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Gate for CLI commands that mutate Prave-side state or whose effect we
|
|
28
|
+
* need to track (installs, deploys, settings, intelligence). Search and
|
|
29
|
+
* read-only Discover paths are intentionally NOT gated — anyone can
|
|
30
|
+
* browse the public registry from the terminal.
|
|
31
|
+
*
|
|
32
|
+
* Prints a friendly hint and sets a non-zero exit code when the caller
|
|
33
|
+
* isn't logged in.
|
|
34
|
+
*/
|
|
35
|
+
export async function requireAuth(commandName) {
|
|
36
|
+
const creds = await loadCredentials();
|
|
37
|
+
if (creds)
|
|
38
|
+
return creds;
|
|
39
|
+
// Lazy-import the chalk/log utilities here to avoid a circular dep on
|
|
40
|
+
// the credentials module.
|
|
41
|
+
const { log } = await import('../utils/logger.js');
|
|
42
|
+
log.warn(`\`${commandName}\` requires sign-in.`);
|
|
43
|
+
log.dim('Run `prave login` first — installs are tracked against your account so we can keep counts honest.');
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.4.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.12.7",
|