@prave/cli 1.4.10 → 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/index.js +14 -0
- package/dist/lib/update-check.js +129 -0
- package/package.json +2 -2
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);
|
|
@@ -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,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",
|