@prave/cli 1.4.9 → 1.4.11
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/login.js +17 -5
- package/dist/commands/settings.js +8 -15
- package/dist/commands/usage.js +3 -3
- package/dist/index.js +21 -7
- package/dist/lib/local-config.js +44 -0
- package/dist/lib/nudge-constants.js +1 -1
- package/dist/lib/nudge.js +25 -24
- package/dist/lib/update-check.js +129 -0
- package/package.json +2 -2
package/dist/commands/login.js
CHANGED
|
@@ -5,6 +5,7 @@ import { track } from '../lib/analytics.js';
|
|
|
5
5
|
import { api, ApiError } from '../lib/api.js';
|
|
6
6
|
import { CONFIG } from '../lib/config.js';
|
|
7
7
|
import { saveCredentials } from '../lib/credentials.js';
|
|
8
|
+
import { readLocalConfig, writeLocalConfigPatch } from '../lib/local-config.js';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
9
10
|
/**
|
|
10
11
|
* `prave login` — device-code flow against the Prave API / Supabase session.
|
|
@@ -51,12 +52,23 @@ export async function loginCommand() {
|
|
|
51
52
|
catch (flushErr) {
|
|
52
53
|
log.dim(`Telemetry sync skipped: ${flushErr.message}`);
|
|
53
54
|
}
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
55
|
+
// First-login onboarding only. The agent picker + hook installer
|
|
56
|
+
// used to fire on EVERY login, which was annoying for users who
|
|
57
|
+
// re-login often (CI environments, multi-machine setups, after a
|
|
58
|
+
// token expiry). Now we gate on a persisted flag in
|
|
59
|
+
// ~/.prave/config.json — once they've completed the picker once,
|
|
60
|
+
// it never auto-fires again. Manual re-run available via
|
|
61
|
+
// `prave settings agent`.
|
|
57
62
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
63
|
+
const cfg = await readLocalConfig();
|
|
64
|
+
if (cfg.agent_onboarding_done !== true) {
|
|
65
|
+
const { runAgentOnboarding } = await import('../lib/agent-onboarding.js');
|
|
66
|
+
await runAgentOnboarding();
|
|
67
|
+
await writeLocalConfigPatch({ agent_onboarding_done: true });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log.dim('Welcome back — run `prave settings agent` to change your agent setup.');
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
catch (onboardErr) {
|
|
62
74
|
log.dim(`Agent onboarding skipped: ${onboardErr.message}`);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
1
|
import { createInterface } from 'node:readline/promises';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import { AGENT_REGISTRY } from '@prave/shared';
|
|
5
4
|
import { api, ApiError } from '../lib/api.js';
|
|
6
5
|
import { requireAuth } from '../lib/credentials.js';
|
|
7
|
-
import {
|
|
6
|
+
import { writeLocalConfigPatch } from '../lib/local-config.js';
|
|
8
7
|
import { log } from '../utils/logger.js';
|
|
9
8
|
function detectOs() {
|
|
10
9
|
if (process.platform === 'darwin')
|
|
@@ -13,20 +12,14 @@ function detectOs() {
|
|
|
13
12
|
return 'windows';
|
|
14
13
|
return 'linux';
|
|
15
14
|
}
|
|
16
|
-
async function loadLocalConfig() {
|
|
17
|
-
try {
|
|
18
|
-
const raw = await readFile(CONFIG.configPath, 'utf8');
|
|
19
|
-
return JSON.parse(raw);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
15
|
async function saveLocalConfig(settings) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await
|
|
16
|
+
// Mark onboarding done as a side effect — running `prave settings
|
|
17
|
+
// agent` manually counts as completing the picker, so the user won't
|
|
18
|
+
// get auto-prompted again on the next `prave login`.
|
|
19
|
+
await writeLocalConfigPatch({
|
|
20
|
+
agentSettings: settings,
|
|
21
|
+
agent_onboarding_done: true,
|
|
22
|
+
});
|
|
30
23
|
}
|
|
31
24
|
async function fetchSettings() {
|
|
32
25
|
const { data } = await api.get('/api/v1/settings/agents', true);
|
package/dist/commands/usage.js
CHANGED
|
@@ -432,8 +432,8 @@ export async function usageStatusCommand() {
|
|
|
432
432
|
const checkmark = (ok) => (ok ? chalk.green('✓') : chalk.red('✗'));
|
|
433
433
|
log.info(chalk.bold('Usage tracking status'));
|
|
434
434
|
console.log();
|
|
435
|
-
console.log(` ${checkmark(toolHookInstalled)}
|
|
436
|
-
console.log(` ${checkmark(promptHookInstalled)}
|
|
435
|
+
console.log(` ${checkmark(toolHookInstalled)} Skill invocation tracking: ${toolHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
|
|
436
|
+
console.log(` ${checkmark(promptHookInstalled)} Slash-command tracking (e.g. /graphify): ${promptHookInstalled ? chalk.green('installed') : chalk.yellow('missing — `prave usage hook install`')}`);
|
|
437
437
|
console.log(` ${checkmark(apiReachable)} API reachable + auth valid: ${apiReachable ? chalk.green('yes') : chalk.red(apiMessage || 'no')}`);
|
|
438
438
|
console.log(` ${checkmark(Boolean(lastScanAt))} Transcript scanner watermark: ${lastScanAt ?? chalk.dim('never run — `prave sync` includes it')}`);
|
|
439
439
|
console.log(` ${checkmark(recent7 > 0)} Events in last 7 days: ${chalk.cyan(String(recent7))}`);
|
|
@@ -470,7 +470,7 @@ export async function usageStatusCommand() {
|
|
|
470
470
|
console.log();
|
|
471
471
|
log.warn('Telemetry may be incomplete. Suggested fixes:');
|
|
472
472
|
if (!toolHookInstalled || !promptHookInstalled) {
|
|
473
|
-
log.dim(' • Run `prave usage hook install` to
|
|
473
|
+
log.dim(' • Run `prave usage hook install` to enable both Skill-fire and slash-command tracking.');
|
|
474
474
|
}
|
|
475
475
|
if (!apiReachable) {
|
|
476
476
|
log.dim(' • Run `prave login` — your access token may have expired (silently 401-ing).');
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { whatdoesCommand } from './commands/whatdoes.js';
|
|
|
25
25
|
import { whoamiCommand } from './commands/whoami.js';
|
|
26
26
|
import { initAnalytics } from './lib/analytics.js';
|
|
27
27
|
import { captureAuthSnapshot, nudgeFirstRun } from './lib/nudge.js';
|
|
28
|
+
import { maybeCheckForUpdate } from './lib/update-check.js';
|
|
28
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
30
31
|
initAnalytics(pkg.version);
|
|
@@ -49,7 +50,7 @@ const program = new Command()
|
|
|
49
50
|
.addHelpText('after', '\nRun `prave <cmd> --help` for command-specific options.');
|
|
50
51
|
program
|
|
51
52
|
.command('login')
|
|
52
|
-
.description('
|
|
53
|
+
.description('Sign this machine in (browser opens, no password typed). Credentials are stored securely on disk and refreshed automatically.')
|
|
53
54
|
.action(loginCommand);
|
|
54
55
|
program
|
|
55
56
|
.command('logout')
|
|
@@ -130,17 +131,17 @@ const usage = program
|
|
|
130
131
|
.description('Track which Skills you actually use (powers the optimiser)');
|
|
131
132
|
usage
|
|
132
133
|
.command('report')
|
|
133
|
-
.description('Internal: invoked by the
|
|
134
|
-
.option('--source <kind>', '
|
|
134
|
+
.description('Internal: invoked by the agent when a Skill fires (reads payload from stdin)')
|
|
135
|
+
.option('--source <kind>', 'event channel that fired this report', 'tool')
|
|
135
136
|
.action((opts) => usageReportCommand(opts));
|
|
136
|
-
const hook = usage.command('hook').description('
|
|
137
|
+
const hook = usage.command('hook').description('Enable / disable real-time Skill invocation tracking');
|
|
137
138
|
hook
|
|
138
139
|
.command('install')
|
|
139
|
-
.description('
|
|
140
|
+
.description('Enable real-time invocation tracking for your installed Skills')
|
|
140
141
|
.action(usageHookInstallCommand);
|
|
141
142
|
hook
|
|
142
143
|
.command('uninstall')
|
|
143
|
-
.description('
|
|
144
|
+
.description('Disable real-time invocation tracking')
|
|
144
145
|
.action(usageHookUninstallCommand);
|
|
145
146
|
program
|
|
146
147
|
.command('mcp-server')
|
|
@@ -188,7 +189,7 @@ program
|
|
|
188
189
|
' prave optimize --remove-unused # delete 30d-silent skills from disk',
|
|
189
190
|
'',
|
|
190
191
|
'Usage tracking (Pro+)',
|
|
191
|
-
' prave usage hook install # real-time
|
|
192
|
+
' prave usage hook install # enable real-time invocation tracking',
|
|
192
193
|
' prave usage hook uninstall',
|
|
193
194
|
'',
|
|
194
195
|
'Settings',
|
|
@@ -217,6 +218,19 @@ program.hook('preAction', async () => {
|
|
|
217
218
|
/* never block the command on nudge bookkeeping */
|
|
218
219
|
}
|
|
219
220
|
});
|
|
221
|
+
// Once-a-day npm update check. Most invocations short-circuit on the
|
|
222
|
+
// 24-hour throttle stored in ~/.prave/config.json; when the throttle
|
|
223
|
+
// has elapsed, a short fetch to the npm registry compares versions
|
|
224
|
+
// and prints a one-line upgrade hint at the end of the command. Hard
|
|
225
|
+
// 2-second timeout so a slow registry can never block the user.
|
|
226
|
+
program.hook('postAction', async () => {
|
|
227
|
+
try {
|
|
228
|
+
await maybeCheckForUpdate(pkg.version);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* nudges + update checks are decorative — never block on them */
|
|
232
|
+
}
|
|
233
|
+
});
|
|
220
234
|
// Global first-run banner. Fires once on the user's very first command
|
|
221
235
|
// regardless of which one it was — catches commands that don't have a
|
|
222
236
|
// per-action nudge wired in (e.g. `prave docs`, `prave conflicts`).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared read / write for `~/.prave/config.json`.
|
|
5
|
+
*
|
|
6
|
+
* The same file holds three classes of state, all merged into one
|
|
7
|
+
* JSON object so callers can patch one key without clobbering the
|
|
8
|
+
* others:
|
|
9
|
+
*
|
|
10
|
+
* • `agentSettings` — written by `prave settings`
|
|
11
|
+
* • Nudge bookkeeping (`first_run`, `nudge_count`) — written by
|
|
12
|
+
* the conversion-nudge module
|
|
13
|
+
* • Onboarding flags (`agent_onboarding_done`) — written by
|
|
14
|
+
* `prave login` so we only walk the user through the agent
|
|
15
|
+
* picker once
|
|
16
|
+
*
|
|
17
|
+
* Previously each consumer reimplemented the same readFile / merge /
|
|
18
|
+
* writeFile dance — three copies in three files. Centralising here
|
|
19
|
+
* means a missing key in one consumer no longer wipes the keys
|
|
20
|
+
* another consumer just wrote.
|
|
21
|
+
*/
|
|
22
|
+
export async function readLocalConfig() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await readFile(CONFIG.configPath, 'utf8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (parsed && typeof parsed === 'object')
|
|
27
|
+
return parsed;
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Merge-write: reads the current config, shallow-merges `patch`, and
|
|
36
|
+
* writes the result. Creates `~/.prave/` with restrictive mode (0700)
|
|
37
|
+
* if it doesn't exist yet.
|
|
38
|
+
*/
|
|
39
|
+
export async function writeLocalConfigPatch(patch) {
|
|
40
|
+
const existing = await readLocalConfig();
|
|
41
|
+
const next = { ...existing, ...patch };
|
|
42
|
+
await mkdir(CONFIG.praveDir, { recursive: true, mode: 0o700 });
|
|
43
|
+
await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
|
|
44
|
+
}
|
|
@@ -47,7 +47,7 @@ export const NUDGE_DEEP = {
|
|
|
47
47
|
bullets: [
|
|
48
48
|
'AI-generated descriptions for all your Skills',
|
|
49
49
|
'Conflict detection across your library',
|
|
50
|
-
'30-day
|
|
50
|
+
'30-day Skill invocation history',
|
|
51
51
|
'Cross-machine sync',
|
|
52
52
|
],
|
|
53
53
|
cta: 'prave.app/signup · free forever · 10 seconds',
|
package/dist/lib/nudge.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
|
-
import { CONFIG } from './config.js';
|
|
4
2
|
import { loadCredentials } from './credentials.js';
|
|
3
|
+
import { readLocalConfig, writeLocalConfigPatch } from './local-config.js';
|
|
5
4
|
import { isAlwaysShow, NUDGE_FIRST_RUN, nudgeFor, } from './nudge-constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Conversion nudges for anonymous CLI users.
|
|
7
|
+
*
|
|
8
|
+
* 4,300 npm-installs, 1 signed-in account. The CLI works for anonymous
|
|
9
|
+
* users by design (lower friction = more installs) but the handoff to
|
|
10
|
+
* the dashboard was missing. These nudges fix that.
|
|
11
|
+
*
|
|
12
|
+
* Rules baked in here, not in the call sites:
|
|
13
|
+
* • Silent for signed-in users. Always.
|
|
14
|
+
* • Soft nudges throttled to ~1 in every 3 commands via a counter
|
|
15
|
+
* persisted to ~/.prave/config.json (`nudge_count`).
|
|
16
|
+
* • Strong nudges (overview/whatdoes/first-run) bypass the throttle —
|
|
17
|
+
* they're the highest-intent moments and worth a fuller pitch.
|
|
18
|
+
* • First-run banner shows once on the user's very first command;
|
|
19
|
+
* `first_run: false` then locks it down forever.
|
|
20
|
+
* • One nudge per process. Multiple commands chained in a wrapper
|
|
21
|
+
* script don't spam.
|
|
22
|
+
* • Skipped when stdout isn't a TTY (piped output, CI) and when
|
|
23
|
+
* PRAVE_QUIET=1 / PRAVE_TELEMETRY=0.
|
|
24
|
+
*/
|
|
6
25
|
let alreadyNudged = false;
|
|
7
26
|
/**
|
|
8
27
|
* Auth snapshot captured at command START, used at command END.
|
|
@@ -35,24 +54,6 @@ export async function isAuthenticated() {
|
|
|
35
54
|
}
|
|
36
55
|
return true;
|
|
37
56
|
}
|
|
38
|
-
async function readConfig() {
|
|
39
|
-
try {
|
|
40
|
-
const raw = await readFile(CONFIG.configPath, 'utf8');
|
|
41
|
-
const parsed = JSON.parse(raw);
|
|
42
|
-
if (parsed && typeof parsed === 'object')
|
|
43
|
-
return parsed;
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return {};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async function writeConfigPatch(patch) {
|
|
51
|
-
const existing = await readConfig();
|
|
52
|
-
const next = { ...existing, ...patch };
|
|
53
|
-
await mkdir(CONFIG.praveDir, { recursive: true, mode: 0o700 });
|
|
54
|
-
await writeFile(CONFIG.configPath, JSON.stringify(next, null, 2), 'utf8');
|
|
55
|
-
}
|
|
56
57
|
/**
|
|
57
58
|
* Counter-based throttle for soft nudges. Reads the current count,
|
|
58
59
|
* increments, persists, and returns true when (count % 3 === 0) — so
|
|
@@ -60,10 +61,10 @@ async function writeConfigPatch(patch) {
|
|
|
60
61
|
* we'd rather skip than be loud on the user's first real command.
|
|
61
62
|
*/
|
|
62
63
|
export async function shouldShowNudge() {
|
|
63
|
-
const cfg = await
|
|
64
|
+
const cfg = await readLocalConfig();
|
|
64
65
|
const current = typeof cfg.nudge_count === 'number' ? cfg.nudge_count : 0;
|
|
65
66
|
const next = current + 1;
|
|
66
|
-
await
|
|
67
|
+
await writeLocalConfigPatch({ nudge_count: next });
|
|
67
68
|
return next % 3 === 0;
|
|
68
69
|
}
|
|
69
70
|
/* --------------------------------------------------------------------- */
|
|
@@ -141,7 +142,7 @@ export async function nudgeFirstRun() {
|
|
|
141
142
|
const startedAnon = wasAnonymousAtStart ?? !(await isAuthenticated());
|
|
142
143
|
if (!startedAnon)
|
|
143
144
|
return false;
|
|
144
|
-
const cfg = await
|
|
145
|
+
const cfg = await readLocalConfig();
|
|
145
146
|
// first_run defaults to "yes, show it" unless we've already flipped
|
|
146
147
|
// the flag. A missing config file → first run.
|
|
147
148
|
const alreadyShown = cfg.first_run === false;
|
|
@@ -149,7 +150,7 @@ export async function nudgeFirstRun() {
|
|
|
149
150
|
return false;
|
|
150
151
|
alreadyNudged = true;
|
|
151
152
|
showNudge(NUDGE_FIRST_RUN);
|
|
152
|
-
await
|
|
153
|
+
await writeLocalConfigPatch({ first_run: false });
|
|
153
154
|
return true;
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readLocalConfig, writeLocalConfigPatch } from './local-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Once-a-day npm update check.
|
|
5
|
+
*
|
|
6
|
+
* A real "first command in a fresh terminal session" hook would need
|
|
7
|
+
* shell integration we don't ship. The user-visible behaviour is
|
|
8
|
+
* identical with a 24-hour timestamp throttle: the first `prave <cmd>`
|
|
9
|
+
* the user runs on any given day reaches out to npm, compares versions,
|
|
10
|
+
* and prints a one-line dim hint if a newer release exists. Subsequent
|
|
11
|
+
* commands inside the same window skip the network round-trip entirely.
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* • Hard cap on round-trip time (2 s). A slow npm registry must NEVER
|
|
15
|
+
* block the user's command. The promise is fire-and-forget from the
|
|
16
|
+
* caller's perspective — we always return cleanly.
|
|
17
|
+
* • Skipped when stdout isn't a TTY (pipes, CI), or when
|
|
18
|
+
* PRAVE_QUIET=1 / PRAVE_TELEMETRY=0 is set.
|
|
19
|
+
* • The check itself runs through `node fetch` so we don't take a
|
|
20
|
+
* dependency on a specific HTTP client (the CLI's `api.ts` is
|
|
21
|
+
* auth-aware and overkill for a public, unauthenticated GET).
|
|
22
|
+
* • Persists `last_update_check` (unix ms) in ~/.prave/config.json
|
|
23
|
+
* regardless of outcome — a network blip is treated like a clean
|
|
24
|
+
* "you're up to date" so we don't re-spam npm every command after
|
|
25
|
+
* a failure.
|
|
26
|
+
*/
|
|
27
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 2_000;
|
|
29
|
+
const NPM_PACKAGE = '@prave/cli';
|
|
30
|
+
/**
|
|
31
|
+
* Returns true if the throttle has elapsed and we should hit npm now.
|
|
32
|
+
* Reads-and-writes the config so concurrent invocations from the same
|
|
33
|
+
* shell don't double-fire (last-write-wins is fine here).
|
|
34
|
+
*/
|
|
35
|
+
async function dueForCheck() {
|
|
36
|
+
const cfg = await readLocalConfig();
|
|
37
|
+
const last = typeof cfg.last_update_check === 'number' ? cfg.last_update_check : 0;
|
|
38
|
+
return Date.now() - last >= CHECK_INTERVAL_MS;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* SemVer-ish comparator. Returns:
|
|
42
|
+
* < 0 if a < b
|
|
43
|
+
* 0 if equal
|
|
44
|
+
* > 0 if a > b
|
|
45
|
+
*
|
|
46
|
+
* Tolerates pre-release tails (`1.4.9-rc.1`) by lexicographically
|
|
47
|
+
* comparing them when the numeric tuples tie. Sufficient for the
|
|
48
|
+
* "newer is available" hint; we don't ship a strict semver library.
|
|
49
|
+
*/
|
|
50
|
+
function compareVersions(a, b) {
|
|
51
|
+
const [aNum, aPre = ''] = a.split('-', 2);
|
|
52
|
+
const [bNum, bPre = ''] = b.split('-', 2);
|
|
53
|
+
const aParts = aNum.split('.').map((n) => parseInt(n, 10));
|
|
54
|
+
const bParts = bNum.split('.').map((n) => parseInt(n, 10));
|
|
55
|
+
const length = Math.max(aParts.length, bParts.length);
|
|
56
|
+
for (let i = 0; i < length; i++) {
|
|
57
|
+
const x = aParts[i] ?? 0;
|
|
58
|
+
const y = bParts[i] ?? 0;
|
|
59
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
60
|
+
return 0; // unparseable → treat as equal
|
|
61
|
+
if (x !== y)
|
|
62
|
+
return x - y;
|
|
63
|
+
}
|
|
64
|
+
// Numeric tuples equal — a release version > any pre-release of the
|
|
65
|
+
// same number ("1.4.9" > "1.4.9-rc.1"), and pre-releases sort by
|
|
66
|
+
// lexicographic order ("1.4.9-rc.1" < "1.4.9-rc.2").
|
|
67
|
+
if (aPre === bPre)
|
|
68
|
+
return 0;
|
|
69
|
+
if (!aPre)
|
|
70
|
+
return 1;
|
|
71
|
+
if (!bPre)
|
|
72
|
+
return -1;
|
|
73
|
+
return aPre < bPre ? -1 : 1;
|
|
74
|
+
}
|
|
75
|
+
async function fetchLatestVersion() {
|
|
76
|
+
// npm's registry returns the latest version in dist-tags without
|
|
77
|
+
// requiring an unauthenticated header; this is the same endpoint
|
|
78
|
+
// `npm view @prave/cli version` hits.
|
|
79
|
+
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(NPM_PACKAGE)}/dist-tags`;
|
|
80
|
+
const ctrl = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
return null;
|
|
86
|
+
const json = (await res.json());
|
|
87
|
+
return typeof json.latest === 'string' ? json.latest : null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Aborted, offline, registry down, JSON broken — treat as "no update".
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function printUpgradeNotice(current, latest) {
|
|
98
|
+
const rule = chalk.dim('─'.repeat(60));
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(rule);
|
|
101
|
+
console.log(`${chalk.yellow('↑')} ${chalk.bold('Prave CLI update available')} ` +
|
|
102
|
+
`${chalk.dim(current)} ${chalk.dim('→')} ${chalk.green(latest)}`);
|
|
103
|
+
console.log(chalk.dim(` Run npm install -g ${NPM_PACKAGE}@latest`));
|
|
104
|
+
console.log(rule);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Public entry. Cheap to call: most invocations short-circuit on the
|
|
108
|
+
* throttle and don't touch the network. Caller is expected to `await`
|
|
109
|
+
* but the promise resolves quickly in the steady state.
|
|
110
|
+
*/
|
|
111
|
+
export async function maybeCheckForUpdate(currentVersion) {
|
|
112
|
+
if (!process.stdout.isTTY)
|
|
113
|
+
return;
|
|
114
|
+
if (process.env.PRAVE_QUIET === '1')
|
|
115
|
+
return;
|
|
116
|
+
if (process.env.PRAVE_TELEMETRY === '0')
|
|
117
|
+
return;
|
|
118
|
+
if (!(await dueForCheck()))
|
|
119
|
+
return;
|
|
120
|
+
// Record the attempt up-front so a network hang doesn't cause us to
|
|
121
|
+
// re-fire on every subsequent command before this one resolves.
|
|
122
|
+
await writeLocalConfigPatch({ last_update_check: Date.now() });
|
|
123
|
+
const latest = await fetchLatestVersion();
|
|
124
|
+
if (!latest)
|
|
125
|
+
return;
|
|
126
|
+
if (compareVersions(latest, currentVersion) <= 0)
|
|
127
|
+
return;
|
|
128
|
+
printUpgradeNotice(currentVersion, latest);
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.11",
|
|
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.11"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^20.12.7",
|