@prave/cli 0.4.10 → 1.0.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/login.js +10 -0
- package/dist/commands/optimize.js +4 -0
- package/dist/commands/overview.js +4 -0
- package/dist/commands/settings.js +13 -31
- package/dist/commands/sync.js +54 -12
- package/dist/commands/update.js +165 -0
- package/dist/commands/usage.js +267 -0
- package/dist/commands/whoami.js +31 -3
- package/dist/index.js +33 -2
- package/dist/lib/agent-onboarding.js +107 -0
- package/dist/lib/api.js +54 -2
- package/dist/lib/credentials.js +21 -0
- package/dist/lib/hook.js +131 -0
- package/dist/lib/prompt.js +124 -0
- package/dist/lib/usage-cursor.js +34 -0
- package/dist/lib/usage-scanner.js +154 -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();
|
package/dist/commands/login.js
CHANGED
|
@@ -33,6 +33,16 @@ export async function loginCommand() {
|
|
|
33
33
|
user_id: data.user_id,
|
|
34
34
|
});
|
|
35
35
|
spinner.succeed('Logged in.');
|
|
36
|
+
// Onboarding: prefill from the SaaS profile, let the user toggle
|
|
37
|
+
// with space/enter, persist back, and offer to install hooks.
|
|
38
|
+
// Failures here are non-fatal — login succeeded.
|
|
39
|
+
try {
|
|
40
|
+
const { runAgentOnboarding } = await import('../lib/agent-onboarding.js');
|
|
41
|
+
await runAgentOnboarding();
|
|
42
|
+
}
|
|
43
|
+
catch (onboardErr) {
|
|
44
|
+
log.dim(`Agent onboarding skipped: ${onboardErr.message}`);
|
|
45
|
+
}
|
|
36
46
|
return;
|
|
37
47
|
}
|
|
38
48
|
catch (err) {
|
|
@@ -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);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { AGENT_REGISTRY
|
|
4
|
+
import { AGENT_REGISTRY } 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() {
|
|
@@ -35,37 +36,15 @@ async function patchSettings(patch) {
|
|
|
35
36
|
const { data } = await api.put('/api/v1/settings/agents', patch, true);
|
|
36
37
|
return data;
|
|
37
38
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const enabled = current.enabled_agents.includes(id) ? chalk.green('✓') : ' ';
|
|
47
|
-
console.log(` ${enabled} ${chalk.cyan(meta.id.padEnd(8))} ${meta.label} ${chalk.dim('— ' + meta.description)}`);
|
|
48
|
-
}
|
|
49
|
-
const answer = (await rl.question(`\nEnter comma-separated agents to enable (default: ${current.enabled_agents.join(',') || 'claude'}): `)).trim();
|
|
50
|
-
let enabled = current.enabled_agents;
|
|
51
|
-
if (answer) {
|
|
52
|
-
const tokens = answer
|
|
53
|
-
.split(',')
|
|
54
|
-
.map((t) => t.trim().toLowerCase())
|
|
55
|
-
.filter(Boolean);
|
|
56
|
-
const invalid = tokens.filter((t) => !isAgentType(t));
|
|
57
|
-
if (invalid.length > 0) {
|
|
58
|
-
log.warn(`Unknown agents skipped: ${invalid.join(', ')}`);
|
|
59
|
-
}
|
|
60
|
-
enabled = tokens.filter(isAgentType);
|
|
61
|
-
if (enabled.length === 0) {
|
|
62
|
-
log.warn('No valid agents selected — keeping previous selection.');
|
|
63
|
-
return current;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const updated = await patchSettings({ enabled_agents: enabled });
|
|
39
|
+
async function configureAgents(_rl, current) {
|
|
40
|
+
// Reuse the shared onboarding flow. Same UX as `prave login`: space to
|
|
41
|
+
// toggle, enter to confirm, plus an opt-in hook install. Persists via
|
|
42
|
+
// PUT /api/v1/settings/agents so web + CLI stay in lockstep.
|
|
43
|
+
const { runAgentOnboarding } = await import('../lib/agent-onboarding.js');
|
|
44
|
+
const updated = await runAgentOnboarding();
|
|
45
|
+
if (!updated)
|
|
46
|
+
return current;
|
|
67
47
|
await saveLocalConfig(updated);
|
|
68
|
-
log.success(`Enabled agents: ${updated.enabled_agents.join(', ')}`);
|
|
69
48
|
return updated;
|
|
70
49
|
}
|
|
71
50
|
async function configurePaths(rl, current) {
|
|
@@ -130,6 +109,9 @@ async function showAccount() {
|
|
|
130
109
|
log.dim('Run `prave whoami` for full identity, or `prave logout` to sign out.');
|
|
131
110
|
}
|
|
132
111
|
export async function settingsCommand() {
|
|
112
|
+
const _session = await requireAuth("prave settings");
|
|
113
|
+
if (!_session)
|
|
114
|
+
return;
|
|
133
115
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
134
116
|
try {
|
|
135
117
|
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,27 @@ 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
|
+
}
|
|
91
|
+
// Tail end of sync: fire a quiet usage scan so the optimiser stays warm
|
|
92
|
+
// without the user having to remember an extra command. Quiet mode means
|
|
93
|
+
// we only log a one-liner ("Usage: 12 new, 88 known.") instead of taking
|
|
94
|
+
// over the spinner. Failures are non-fatal — sync's primary job is done.
|
|
95
|
+
try {
|
|
96
|
+
const { usageScanCommand } = await import('./usage.js');
|
|
97
|
+
await usageScanCommand({ quiet: true });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* swallow — usage tracking is best-effort */
|
|
101
|
+
}
|
|
60
102
|
}
|
|
@@ -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
|
+
}
|