@vike-io/cli 0.5.0 → 0.6.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/CHANGELOG.md +26 -0
- package/package.json +3 -1
- package/scripts/postinstall.js +36 -16
- package/src/chains.js +86 -0
- package/src/index.js +24 -5
- package/src/telemetry.js +120 -0
- package/src/update-check.js +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented here. Entries follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
4
|
|
|
5
|
+
## 0.6.0 — infra hardening + observability + skill eval harness
|
|
6
|
+
|
|
7
|
+
Mostly internal-quality improvements; no new commands. Backbone for everything that follows.
|
|
8
|
+
|
|
9
|
+
**New infra**
|
|
10
|
+
- **Background update-check** — silent detached subprocess polls the registry once/day; next-run prints `Update available: X.Y.Z → A.B.C`. Honors `NO_UPDATE_NOTIFIER` / `CI` / `VIKE_NO_UPDATE_CHECK`.
|
|
11
|
+
- **First-run-after-upgrade notice** — one-time `Updated to X.Y.Z` print.
|
|
12
|
+
- **Telemetry skeleton** — anonymous UUID + 30-min session ID, fire-and-forget. NO sink endpoint wired yet (events drop silently); shape is stable so we can flip it on without refactoring. Opt-out via `DO_NOT_TRACK=1` or `VIKE_NO_TELEMETRY=1`.
|
|
13
|
+
- **Centralized chain registry** (`src/chains.js`) — `ALL_CHAINS`, `EVM_CHAINS`, `WALLET_CHAINS`, `CHAIN_ALIASES`, `normaliseChain()`, `isEvmAddress()`.
|
|
14
|
+
|
|
15
|
+
**Testing + release safety**
|
|
16
|
+
- Two-tier vitest config — `npm test` (unit/integration, <30s) vs `npm run test:e2e` (live MCP, up to 2 min).
|
|
17
|
+
- Four meta-tests added: shebang, pack-and-install (catches missing `files`), CLI help for every command, command coverage regression.
|
|
18
|
+
- Pretest changeset validator — hard-fails on wrong package name in changeset frontmatter; warns on missing changeset.
|
|
19
|
+
- Postinstall script hardened — global-install + TTY gates, always `exit 0`.
|
|
20
|
+
|
|
21
|
+
**Skill quality evals (preview)**
|
|
22
|
+
- `evals/` directory with a `uv run`-script harness: A/B-tests every question against `vike --help` only vs `vike --help + SKILL.md`. Scores per question; aggregates a Δ for each skill. If a skill doesn't move the score, it shouldn't ship. Run: `uv run --script evals/runner.py` (needs `ANTHROPIC_API_KEY`).
|
|
23
|
+
- 14 starter questions in `evals/questions.yaml`.
|
|
24
|
+
|
|
25
|
+
**Security**
|
|
26
|
+
- `SECURITY.md` published (report at security@vike.io; 48h ack, 7d critical-fix SLA).
|
|
27
|
+
- `CODEOWNERS` file (single-maintainer for now; security-sensitive paths called out).
|
|
28
|
+
- Dependabot weekly PRs for npm + GitHub Actions deps.
|
|
29
|
+
- `npm audit --audit-level=high` added to CI.
|
|
30
|
+
|
|
5
31
|
## 0.5.0 — initial public release
|
|
6
32
|
|
|
7
33
|
AI-agent CLI for vike.io on-chain analytics, perpetuals, options flow, and prediction-market data across 11 chains.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.0",
|
|
7
7
|
"description": "AI-agent CLI for vike.io on-chain analytics, perps, options, and prediction markets across 11 chains",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"postinstall": "node scripts/postinstall.js",
|
|
21
21
|
"start": "node src/index.js",
|
|
22
|
+
"pretest": "node scripts/check-changeset.js",
|
|
22
23
|
"test": "vitest run --passWithNoTests",
|
|
23
24
|
"test:watch": "vitest",
|
|
25
|
+
"test:e2e": "vitest run --config vitest.e2e.config.js",
|
|
24
26
|
"lint": "eslint .",
|
|
25
27
|
"lint:fix": "eslint . --fix",
|
|
26
28
|
"changeset": "changeset",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
// Postinstall — print a friendly setup hint
|
|
2
|
-
//
|
|
1
|
+
// Postinstall — print a friendly setup hint on global install only.
|
|
2
|
+
//
|
|
3
|
+
// Three defensive guards:
|
|
4
|
+
// 1. Local installs (`npm i @vike-io/cli` inside a project) get nothing —
|
|
5
|
+
// the hint would be noise. Only global installs (`-g`) print.
|
|
6
|
+
// 2. Non-TTY environments (CI, scripts, docker build) get nothing —
|
|
7
|
+
// nothing in here is interactive, but defensively short-circuit so a
|
|
8
|
+
// future maintainer can't accidentally prompt and hang `npm ci`.
|
|
9
|
+
// 3. Any error is swallowed — postinstall must NEVER break `npm install`.
|
|
10
|
+
//
|
|
11
|
+
// Honors $CI and npm's --silent / loglevel=silent.
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
const isGlobal = String(process.env.npm_config_global || '') === 'true';
|
|
14
|
+
const hasTTY = Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
15
|
+
const isQuiet = process.env.CI ||
|
|
16
|
+
process.env.npm_config_loglevel === 'silent' ||
|
|
17
|
+
process.env.npm_config_loglevel === 'error';
|
|
18
|
+
|
|
19
|
+
if (!isGlobal || !hasTTY || isQuiet) {
|
|
5
20
|
process.exit(0);
|
|
6
21
|
}
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
try {
|
|
24
|
+
const lines = [
|
|
25
|
+
'',
|
|
26
|
+
' @vike-io/cli installed.',
|
|
27
|
+
'',
|
|
28
|
+
' Next steps:',
|
|
29
|
+
' vike init — interactive setup',
|
|
30
|
+
' vike doctor — verify your install',
|
|
31
|
+
' vike token search BTC — your first query',
|
|
32
|
+
'',
|
|
33
|
+
' Docs: https://vike.io/api',
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
process.stdout.write(lines.join('\n'));
|
|
37
|
+
} catch {
|
|
38
|
+
// Postinstall must not fail an npm install. Swallow everything.
|
|
39
|
+
}
|
|
40
|
+
process.exit(0);
|
package/src/chains.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Central chain registry. Every command that takes a --chain flag should
|
|
2
|
+
// import EVM_CHAINS or ALL_CHAINS from here. Adding a chain = one edit.
|
|
3
|
+
|
|
4
|
+
export const EVM_CHAINS = [
|
|
5
|
+
'ethereum',
|
|
6
|
+
'bsc',
|
|
7
|
+
'base',
|
|
8
|
+
'arbitrum',
|
|
9
|
+
'polygon',
|
|
10
|
+
'optimism',
|
|
11
|
+
'hyperevm',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const NON_EVM_CHAINS = [
|
|
15
|
+
'bitcoin',
|
|
16
|
+
'solana',
|
|
17
|
+
'tron',
|
|
18
|
+
'xmr',
|
|
19
|
+
'zec',
|
|
20
|
+
'dash',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const ALL_CHAINS = [...EVM_CHAINS, ...NON_EVM_CHAINS];
|
|
24
|
+
|
|
25
|
+
// Chains the wallet_* tools currently support (subset of EVM_CHAINS where
|
|
26
|
+
// we have indexed transfers + labels). Used by --chain validation in
|
|
27
|
+
// commands like `wallet summary` / `wallet counterparties` / etc.
|
|
28
|
+
export const WALLET_CHAINS = ['ethereum', 'bsc', 'base'];
|
|
29
|
+
|
|
30
|
+
// Chain abbreviation map: short → canonical. We accept both forms from
|
|
31
|
+
// CLI input and normalise to canonical before sending to MCP.
|
|
32
|
+
export const CHAIN_ALIASES = {
|
|
33
|
+
eth: 'ethereum',
|
|
34
|
+
ethereum: 'ethereum',
|
|
35
|
+
bsc: 'bsc',
|
|
36
|
+
bnb: 'bsc',
|
|
37
|
+
base: 'base',
|
|
38
|
+
arb: 'arbitrum',
|
|
39
|
+
arbitrum: 'arbitrum',
|
|
40
|
+
poly: 'polygon',
|
|
41
|
+
polygon: 'polygon',
|
|
42
|
+
op: 'optimism',
|
|
43
|
+
optimism: 'optimism',
|
|
44
|
+
hyper: 'hyperevm',
|
|
45
|
+
hyperevm: 'hyperevm',
|
|
46
|
+
btc: 'bitcoin',
|
|
47
|
+
bitcoin: 'bitcoin',
|
|
48
|
+
sol: 'solana',
|
|
49
|
+
solana: 'solana',
|
|
50
|
+
tron: 'tron',
|
|
51
|
+
trx: 'tron',
|
|
52
|
+
xmr: 'xmr',
|
|
53
|
+
monero: 'xmr',
|
|
54
|
+
zec: 'zec',
|
|
55
|
+
zcash: 'zec',
|
|
56
|
+
dash: 'dash',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function normaliseChain(input, fallback = 'ethereum') {
|
|
60
|
+
if (!input) return fallback;
|
|
61
|
+
const key = String(input).toLowerCase().trim();
|
|
62
|
+
return CHAIN_ALIASES[key] ?? input;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Address-format expectations per chain. Used by `vike doctor` and
|
|
66
|
+
// future input validation. EVM = 0x + 40 hex; Solana = base58 32-44 chars;
|
|
67
|
+
// BTC = various encodings; privacy chains = chain-specific.
|
|
68
|
+
export const ADDRESS_FORMATS = {
|
|
69
|
+
ethereum: '0x + 40 hex chars',
|
|
70
|
+
bsc: '0x + 40 hex chars',
|
|
71
|
+
base: '0x + 40 hex chars',
|
|
72
|
+
arbitrum: '0x + 40 hex chars',
|
|
73
|
+
polygon: '0x + 40 hex chars',
|
|
74
|
+
optimism: '0x + 40 hex chars',
|
|
75
|
+
hyperevm: '0x + 40 hex chars',
|
|
76
|
+
solana: 'base58, 32-44 chars',
|
|
77
|
+
bitcoin: 'bech32 / P2PKH / P2SH',
|
|
78
|
+
tron: 'base58 starting with T',
|
|
79
|
+
xmr: 'monero address (95 chars)',
|
|
80
|
+
zec: 'zcash transparent or shielded',
|
|
81
|
+
dash: 'P2PKH starting with X',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function isEvmAddress(addr) {
|
|
85
|
+
return typeof addr === 'string' && /^0x[0-9a-fA-F]{40}$/.test(addr);
|
|
86
|
+
}
|
package/src/index.js
CHANGED
|
@@ -16,13 +16,27 @@ import { registerDoctor } from './commands/doctor.js';
|
|
|
16
16
|
import { registerSchema } from './commands/schema.js';
|
|
17
17
|
import { registerLogin, registerLogout } from './commands/auth.js';
|
|
18
18
|
import { readPackageVersion } from './util.js';
|
|
19
|
+
import {
|
|
20
|
+
getUpdateNotification,
|
|
21
|
+
getUpgradeNotice,
|
|
22
|
+
scheduleUpdateCheck,
|
|
23
|
+
} from './update-check.js';
|
|
24
|
+
|
|
25
|
+
const version = readPackageVersion();
|
|
26
|
+
|
|
27
|
+
// Print at-most-once notifications: upgrade-since-last-run, and update-available
|
|
28
|
+
// (the latter from the previous background check). Cheap, sync, exits if quiet.
|
|
29
|
+
const upgradeNotice = getUpgradeNotice(version);
|
|
30
|
+
const updateNotice = getUpdateNotification(version);
|
|
31
|
+
if (upgradeNotice) process.stderr.write(upgradeNotice);
|
|
32
|
+
if (updateNotice) process.stderr.write(updateNotice);
|
|
19
33
|
|
|
20
34
|
const program = new Command();
|
|
21
35
|
|
|
22
36
|
program
|
|
23
37
|
.name('vike')
|
|
24
38
|
.description('AI-agent CLI for vike.io on-chain analytics')
|
|
25
|
-
.version(
|
|
39
|
+
.version(version, '-v, --version', 'Show CLI version');
|
|
26
40
|
|
|
27
41
|
registerLogin(program);
|
|
28
42
|
registerLogout(program);
|
|
@@ -38,7 +52,12 @@ registerPolymarket(program);
|
|
|
38
52
|
registerLabels(program);
|
|
39
53
|
registerWeb(program);
|
|
40
54
|
|
|
41
|
-
program.parseAsync(process.argv)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
program.parseAsync(process.argv)
|
|
56
|
+
.catch((err) => {
|
|
57
|
+
console.error(`vike: ${err.message ?? err}`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
// Fire-and-forget background check, detached subprocess. Survives our exit.
|
|
62
|
+
scheduleUpdateCheck();
|
|
63
|
+
});
|
package/src/telemetry.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Anonymous, opt-out telemetry skeleton.
|
|
2
|
+
//
|
|
3
|
+
// Design decisions:
|
|
4
|
+
// - Honors DO_NOT_TRACK=1 (industry standard) AND VIKE_NO_TELEMETRY=1
|
|
5
|
+
// - Honors CI / NODE_ENV=test (never phones home from CI)
|
|
6
|
+
// - Persistent UUIDv4 in ~/.vike/telemetry-id (regenerated on delete)
|
|
7
|
+
// - Rolling 30-min session ID — same session_id within a 30-min window
|
|
8
|
+
// - AbortController + 1000ms timeout, timer.unref() so we can't keep
|
|
9
|
+
// the process alive
|
|
10
|
+
// - Payload includes: flag NAMES (no values), latency, success/error_code,
|
|
11
|
+
// chain (if relevant). NEVER the actual data the user queried.
|
|
12
|
+
// - Fire-and-forget: errors are swallowed
|
|
13
|
+
//
|
|
14
|
+
// THE SINK IS NOT YET WIRED. `_send()` is a no-op until we have an
|
|
15
|
+
// endpoint. Keeping the API shape stable now so we can flip the switch
|
|
16
|
+
// later without a refactor.
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
const CONFIG_DIR = join(homedir(), '.vike');
|
|
24
|
+
const ID_FILE = join(CONFIG_DIR, 'telemetry-id');
|
|
25
|
+
const SESSION_FILE = join(CONFIG_DIR, 'session');
|
|
26
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
27
|
+
const ENDPOINT = process.env.VIKE_TELEMETRY_ENDPOINT || '';
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 1000;
|
|
29
|
+
|
|
30
|
+
function disabled() {
|
|
31
|
+
return process.env.DO_NOT_TRACK === '1' ||
|
|
32
|
+
process.env.VIKE_NO_TELEMETRY === '1' ||
|
|
33
|
+
process.env.CI ||
|
|
34
|
+
process.env.NODE_ENV === 'test';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir() {
|
|
38
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
39
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDeviceId() {
|
|
44
|
+
try {
|
|
45
|
+
if (existsSync(ID_FILE)) return readFileSync(ID_FILE, 'utf8').trim();
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
try { ensureDir(); writeFileSync(ID_FILE, id, { mode: 0o600 }); } catch { /* ignore */ }
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSessionId() {
|
|
53
|
+
let prev = null;
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(SESSION_FILE)) {
|
|
56
|
+
prev = JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
// Reuse session if within TTL; cheaply update touched timestamp at most
|
|
61
|
+
// once per minute to avoid hammering the file on rapid commands.
|
|
62
|
+
if (prev && prev.id && now - (prev.touched || 0) < SESSION_TTL_MS) {
|
|
63
|
+
if (now - (prev.touched || 0) > 60_000) {
|
|
64
|
+
try { writeFileSync(SESSION_FILE, JSON.stringify({ id: prev.id, touched: now })); }
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
return prev.id;
|
|
68
|
+
}
|
|
69
|
+
const id = randomUUID();
|
|
70
|
+
try {
|
|
71
|
+
ensureDir();
|
|
72
|
+
writeFileSync(SESSION_FILE, JSON.stringify({ id, touched: now }));
|
|
73
|
+
} catch { /* ignore */ }
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _send(event, payload) {
|
|
78
|
+
if (disabled() || !ENDPOINT) return;
|
|
79
|
+
// Fire-and-forget. No await; can't keep the process alive.
|
|
80
|
+
const ctrl = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
82
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
83
|
+
fetch(ENDPOINT, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
event,
|
|
88
|
+
device_id: getDeviceId(),
|
|
89
|
+
session_id: getSessionId(),
|
|
90
|
+
ts: Date.now(),
|
|
91
|
+
...payload,
|
|
92
|
+
}),
|
|
93
|
+
signal: ctrl.signal,
|
|
94
|
+
})
|
|
95
|
+
.catch(() => { /* fire-and-forget */ })
|
|
96
|
+
.finally(() => clearTimeout(timer));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function flagNames(opts) {
|
|
100
|
+
// Extract flag names from a commander options object. Values stripped.
|
|
101
|
+
if (!opts || typeof opts !== 'object') return [];
|
|
102
|
+
return Object.keys(opts).filter((k) => !k.startsWith('_') && k !== 'parent');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function trackCommandSucceeded(command, opts, { durationMs } = {}) {
|
|
106
|
+
_send('command_succeeded', {
|
|
107
|
+
command,
|
|
108
|
+
flags: flagNames(opts),
|
|
109
|
+
duration_ms: typeof durationMs === 'number' ? Math.round(durationMs) : null,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function trackCommandFailed(command, opts, { errorCode, durationMs } = {}) {
|
|
114
|
+
_send('command_failed', {
|
|
115
|
+
command,
|
|
116
|
+
flags: flagNames(opts),
|
|
117
|
+
error_code: errorCode || 'unknown',
|
|
118
|
+
duration_ms: typeof durationMs === 'number' ? Math.round(durationMs) : null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Background update check — reads cached result synchronously on startup,
|
|
2
|
+
// kicks off the registry probe as a detached subprocess that survives
|
|
3
|
+
// process exit. Zero blocking I/O on the user's command.
|
|
4
|
+
//
|
|
5
|
+
// Pattern: print a one-liner the *next* time the user runs us, after the
|
|
6
|
+
// background check has cached the result. No "checking for updates..."
|
|
7
|
+
// spinner ever appears.
|
|
8
|
+
//
|
|
9
|
+
// Honors NO_UPDATE_NOTIFIER and CI (industry-standard opt-out).
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = join(homedir(), '.vike');
|
|
18
|
+
const CHECK_FILE = join(CONFIG_DIR, 'update-check.json');
|
|
19
|
+
const LAST_VERSION_FILE = join(CONFIG_DIR, 'last-version.json');
|
|
20
|
+
const PACKAGE_NAME = '@vike-io/cli';
|
|
21
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
22
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
23
|
+
|
|
24
|
+
function suppressed() {
|
|
25
|
+
return process.env.NO_UPDATE_NOTIFIER === '1' ||
|
|
26
|
+
process.env.CI ||
|
|
27
|
+
process.env.NODE_ENV === 'test' ||
|
|
28
|
+
process.env.VIKE_NO_UPDATE_CHECK === '1';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readCache() {
|
|
32
|
+
if (!existsSync(CHECK_FILE)) return null;
|
|
33
|
+
try { return JSON.parse(readFileSync(CHECK_FILE, 'utf8')); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeCache(latest) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
39
|
+
writeFileSync(CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }, null, 2));
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cmpSemver(a, b) {
|
|
44
|
+
const pa = a.split('.').map(Number);
|
|
45
|
+
const pb = b.split('.').map(Number);
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
48
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Synchronous: read whatever the background process last cached.
|
|
54
|
+
// Returns a notification string or null. Call at startup.
|
|
55
|
+
export function getUpdateNotification(currentVersion) {
|
|
56
|
+
if (suppressed()) return null;
|
|
57
|
+
const cache = readCache();
|
|
58
|
+
if (!cache || !cache.latest) return null;
|
|
59
|
+
if (cmpSemver(cache.latest, currentVersion) > 0) {
|
|
60
|
+
return (
|
|
61
|
+
`\n Update available: ${currentVersion} → ${cache.latest}\n` +
|
|
62
|
+
` Run: npm install -g ${PACKAGE_NAME}\n`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fire the check as a detached subprocess. Doesn't block process exit.
|
|
69
|
+
// Honors a 1-day cache window — re-checking on every command would
|
|
70
|
+
// hammer the npm registry.
|
|
71
|
+
export function scheduleUpdateCheck() {
|
|
72
|
+
if (suppressed()) return;
|
|
73
|
+
const cache = readCache();
|
|
74
|
+
if (cache && Date.now() - (cache.checkedAt || 0) < CHECK_INTERVAL_MS) return;
|
|
75
|
+
|
|
76
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
77
|
+
// Inline child script — no separate file needed, no dependency on
|
|
78
|
+
// package layout, survives `npm install -g` symlinking.
|
|
79
|
+
const code = `
|
|
80
|
+
const https = require('https');
|
|
81
|
+
const fs = require('fs');
|
|
82
|
+
const path = require('path');
|
|
83
|
+
const os = require('os');
|
|
84
|
+
const file = path.join(os.homedir(), '.vike', 'update-check.json');
|
|
85
|
+
https.get('${REGISTRY}/${PACKAGE_NAME}/latest', { timeout: 5000 }, (r) => {
|
|
86
|
+
let buf = '';
|
|
87
|
+
r.on('data', (c) => { buf += c; });
|
|
88
|
+
r.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const v = JSON.parse(buf).version;
|
|
91
|
+
if (!v) return;
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
93
|
+
fs.writeFileSync(file, JSON.stringify({ latest: v, checkedAt: Date.now() }));
|
|
94
|
+
} catch {}
|
|
95
|
+
});
|
|
96
|
+
}).on('error', () => {}).on('timeout', function () { this.destroy(); });
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const child = spawn(process.execPath, ['-e', code], {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
});
|
|
104
|
+
child.unref();
|
|
105
|
+
} catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// First-run-after-upgrade hook. Stores last-seen version; if it changes,
|
|
109
|
+
// prints a one-time "you upgraded" notice on the next command.
|
|
110
|
+
export function getUpgradeNotice(currentVersion) {
|
|
111
|
+
if (suppressed()) return null;
|
|
112
|
+
let last = null;
|
|
113
|
+
try {
|
|
114
|
+
if (existsSync(LAST_VERSION_FILE)) {
|
|
115
|
+
last = JSON.parse(readFileSync(LAST_VERSION_FILE, 'utf8')).version;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
try {
|
|
119
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
120
|
+
writeFileSync(LAST_VERSION_FILE, JSON.stringify({ version: currentVersion }));
|
|
121
|
+
} catch { /* ignore */ }
|
|
122
|
+
if (last && last !== currentVersion && cmpSemver(currentVersion, last) > 0) {
|
|
123
|
+
return `\n Updated to ${currentVersion} (was ${last}).\n`;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|