depvitals 0.1.2 → 0.2.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/README.md +37 -55
- package/dist/cli/entry.d.ts +59 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/entry.js +95 -0
- package/dist/cli/entry.js.map +1 -0
- package/dist/cli/index.js +14 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/notifier/eligibility.d.ts +65 -0
- package/dist/cli/notifier/eligibility.d.ts.map +1 -0
- package/dist/cli/notifier/eligibility.js +123 -0
- package/dist/cli/notifier/eligibility.js.map +1 -0
- package/dist/cli/notifier/registryLookup.d.ts +111 -0
- package/dist/cli/notifier/registryLookup.d.ts.map +1 -0
- package/dist/cli/notifier/registryLookup.js +211 -0
- package/dist/cli/notifier/registryLookup.js.map +1 -0
- package/dist/cli/notifier/updateCache.d.ts +101 -0
- package/dist/cli/notifier/updateCache.d.ts.map +1 -0
- package/dist/cli/notifier/updateCache.js +352 -0
- package/dist/cli/notifier/updateCache.js.map +1 -0
- package/dist/cli/notifier/updateCheck.d.ts +88 -0
- package/dist/cli/notifier/updateCheck.d.ts.map +1 -0
- package/dist/cli/notifier/updateCheck.js +166 -0
- package/dist/cli/notifier/updateCheck.js.map +1 -0
- package/dist/cli/notifier/versionCompare.d.ts +48 -0
- package/dist/cli/notifier/versionCompare.d.ts.map +1 -0
- package/dist/cli/notifier/versionCompare.js +89 -0
- package/dist/cli/notifier/versionCompare.js.map +1 -0
- package/dist/cli/runCli.d.ts +2 -0
- package/dist/cli/runCli.d.ts.map +1 -1
- package/dist/cli/runCli.js +30 -3
- package/dist/cli/runCli.js.map +1 -1
- package/dist/core/analyzeProject.d.ts +44 -6
- package/dist/core/analyzeProject.d.ts.map +1 -1
- package/dist/core/analyzeProject.js +110 -11
- package/dist/core/analyzeProject.js.map +1 -1
- package/dist/detectors/eslintManifestConfig.d.ts +111 -0
- package/dist/detectors/eslintManifestConfig.d.ts.map +1 -0
- package/dist/detectors/eslintManifestConfig.js +348 -0
- package/dist/detectors/eslintManifestConfig.js.map +1 -0
- package/dist/detectors/eslintRcJsonConfig.d.ts +85 -0
- package/dist/detectors/eslintRcJsonConfig.d.ts.map +1 -0
- package/dist/detectors/eslintRcJsonConfig.js +188 -0
- package/dist/detectors/eslintRcJsonConfig.js.map +1 -0
- package/dist/detectors/jestManifestConfig.d.ts +76 -0
- package/dist/detectors/jestManifestConfig.d.ts.map +1 -0
- package/dist/detectors/jestManifestConfig.js +292 -0
- package/dist/detectors/jestManifestConfig.js.map +1 -0
- package/dist/reporters/terminalReporter.js +27 -1
- package/dist/reporters/terminalReporter.js.map +1 -1
- package/dist/scanner/moduleSpecifiers.d.ts +9 -1
- package/dist/scanner/moduleSpecifiers.d.ts.map +1 -1
- package/dist/scanner/moduleSpecifiers.js +2352 -173
- package/dist/scanner/moduleSpecifiers.js.map +1 -1
- package/dist/scanner/projectManifest.d.ts +14 -0
- package/dist/scanner/projectManifest.d.ts.map +1 -1
- package/dist/scanner/projectManifest.js +40 -0
- package/dist/scanner/projectManifest.js.map +1 -1
- package/dist/scanner/scriptUsage.js +35 -3
- package/dist/scanner/scriptUsage.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort 24-hour cache for the optional CLI update check (Step 65).
|
|
3
|
+
*
|
|
4
|
+
* The update-notifier contract allows **at most one** bounded public registry lookup per user per
|
|
5
|
+
* day, so the version reported by a previous run is remembered in a tiny private cache file:
|
|
6
|
+
*
|
|
7
|
+
* ```json
|
|
8
|
+
* { "latest": "0.2.1", "checkedAt": 1792170000000 }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Design rules, all of them derived from the contract and from the repository invariants:
|
|
12
|
+
*
|
|
13
|
+
* - **Outside every analyzed project.** The path is always resolved from a user-scoped environment
|
|
14
|
+
* variable or from the user's home directory, and a candidate that is not absolute is rejected.
|
|
15
|
+
* No part of the path is ever derived from the scanned directory, from `cwd`, or from any project
|
|
16
|
+
* file, so scanning a project named `depvitals` - or running from any working directory - cannot
|
|
17
|
+
* place the cache inside that project.
|
|
18
|
+
* - **Minimal, private content.** Only the public `latest` version string and the timestamp of the
|
|
19
|
+
* check are stored: no project path, no manifest content, no dependency list, no credential, no
|
|
20
|
+
* machine identifier and no telemetry. The directory is created `0o700` and the file `0o600` on
|
|
21
|
+
* platforms that honor POSIX mode bits.
|
|
22
|
+
* - **Untrusted on read.** Cached data is treated exactly like untrusted registry data: missing
|
|
23
|
+
* file, unreadable file, oversized file, non-regular file (including a symlink), invalid JSON,
|
|
24
|
+
* wrong types, unexpected extra keys, a negative timestamp and a timestamp in the future are all
|
|
25
|
+
* cache *misses*, never errors.
|
|
26
|
+
* - **Invisible on failure.** Every filesystem operation is wrapped so that permission denied,
|
|
27
|
+
* read-only media, a missing parent directory or a failing rename resolve normally instead of
|
|
28
|
+
* throwing. Nothing here writes to stdout/stderr, and nothing here can change scan output or the
|
|
29
|
+
* process exit code.
|
|
30
|
+
*
|
|
31
|
+
* The module performs no network access, decides no eligibility, prints nothing and is deliberately
|
|
32
|
+
* not wired into the CLI yet: the update check is assembled in Step 67, and the registry lookup
|
|
33
|
+
* itself belongs to Step 66. Because both helpers are asynchronous, callers must await them so a
|
|
34
|
+
* short-lived process does not exit before the cache write finishes.
|
|
35
|
+
*/
|
|
36
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
import { posix, win32 } from 'node:path';
|
|
39
|
+
/**
|
|
40
|
+
* The freshness window. An entry is usable while its age is `>= 0` and strictly below this value;
|
|
41
|
+
* an older entry is a miss, which is what allows a later step to refresh it.
|
|
42
|
+
*/
|
|
43
|
+
export const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
44
|
+
/** Name of the cache file, exported so callers and documentation can name the exact path. */
|
|
45
|
+
export const UPDATE_CACHE_FILE_NAME = 'update-check.json';
|
|
46
|
+
/** The one environment variable consulted on Linux/macOS/other POSIX systems. */
|
|
47
|
+
const XDG_CACHE_HOME = 'XDG_CACHE_HOME';
|
|
48
|
+
/** The one environment variable consulted on Windows. */
|
|
49
|
+
const LOCAL_APP_DATA = 'LOCALAPPDATA';
|
|
50
|
+
/** Cache subdirectory added to the resolved user cache root. */
|
|
51
|
+
const CACHE_DIRECTORY_NAME = 'depvitals';
|
|
52
|
+
/** Home-directory fallback used when no usable environment variable exists: `~/.cache/depvitals`. */
|
|
53
|
+
const HOME_CACHE_SEGMENTS = ['.cache', CACHE_DIRECTORY_NAME];
|
|
54
|
+
/**
|
|
55
|
+
* Upper bound of the accepted cache file, in bytes. A real entry is roughly 50 bytes, so a larger
|
|
56
|
+
* file cannot be our own record and is refused *before* it is read or parsed.
|
|
57
|
+
*/
|
|
58
|
+
const MAX_CACHE_BYTES = 8 * 1024;
|
|
59
|
+
/** Upper bound of the stored version identifier, which keeps a hostile cache file tiny. */
|
|
60
|
+
const MAX_VERSION_LENGTH = 64;
|
|
61
|
+
/**
|
|
62
|
+
* Characters that can never appear in the stored identifier: whitespace, control characters, a
|
|
63
|
+
* quote and a backslash. This is *not* a version grammar - exact stable-version syntax is owned by
|
|
64
|
+
* `versionCompare.isNewerStableVersion`, which stays the single source of truth - it only rejects
|
|
65
|
+
* values that cannot be used as an identifier and keeps the JSON record well-formed.
|
|
66
|
+
*/
|
|
67
|
+
const UNUSABLE_IDENTIFIER = /[\s"\\/]/;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `value` contains a C0/C1-in-ASCII-range control character. Written as a code-point scan
|
|
70
|
+
* instead of a regex range so the lint rule against control-character patterns stays meaningful.
|
|
71
|
+
*/
|
|
72
|
+
function hasControlCharacter(value) {
|
|
73
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
74
|
+
const code = value.charCodeAt(index);
|
|
75
|
+
if (code < 0x20 || code === 0x7f) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/** The only mode bits requested for the cache directory and the cache file. */
|
|
82
|
+
const CACHE_DIRECTORY_MODE = 0o700;
|
|
83
|
+
const CACHE_FILE_MODE = 0o600;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the Windows path dialect applies. Only the exact `'win32'` platform string (the value
|
|
86
|
+
* `process.platform` reports on Windows) selects it; every other platform uses POSIX rules.
|
|
87
|
+
*/
|
|
88
|
+
function isWindowsPlatform(platform) {
|
|
89
|
+
return (platform ?? process.platform) === 'win32';
|
|
90
|
+
}
|
|
91
|
+
/** The path helpers matching the platform of one call. */
|
|
92
|
+
function pathsFor(platform) {
|
|
93
|
+
return isWindowsPlatform(platform) ? win32 : posix;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Accept a directory candidate only when it is a non-empty absolute path. Relative values - which
|
|
97
|
+
* would silently be resolved against the current working directory, and therefore possibly inside
|
|
98
|
+
* the project being scanned - are rejected here, and a rejected location turns into a cache miss.
|
|
99
|
+
*/
|
|
100
|
+
function absoluteOnly(candidate, paths) {
|
|
101
|
+
if (candidate === undefined || candidate === '') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return paths.isAbsolute(candidate) ? candidate : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read the home directory from the injected provider, or from `node:os`. A provider that throws or
|
|
108
|
+
* returns an empty/relative value yields no usable location.
|
|
109
|
+
*/
|
|
110
|
+
function homeOnly(options, paths) {
|
|
111
|
+
let reported;
|
|
112
|
+
try {
|
|
113
|
+
reported = options.homedir?.() ?? homedir();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// An unavailable home directory is a normal situation (locked-down or daemon accounts).
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return absoluteOnly(reported, paths);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the user-scoped cache *directory* for the platform:
|
|
123
|
+
*
|
|
124
|
+
* - Windows: absolute `LOCALAPPDATA` + `depvitals`;
|
|
125
|
+
* - otherwise: absolute `XDG_CACHE_HOME` + `depvitals`;
|
|
126
|
+
* - fallback on every platform: `<home>/.cache/depvitals`.
|
|
127
|
+
*
|
|
128
|
+
* Environment variables are consulted by exact name, are only accepted when absolute, and never
|
|
129
|
+
* fall back to the analyzed project.
|
|
130
|
+
*/
|
|
131
|
+
function cacheDirectory(options, paths) {
|
|
132
|
+
const override = absoluteOnly(options.cacheDir, paths);
|
|
133
|
+
if (override !== undefined) {
|
|
134
|
+
return override;
|
|
135
|
+
}
|
|
136
|
+
const env = options.env ?? {};
|
|
137
|
+
if (isWindowsPlatform(options.platform)) {
|
|
138
|
+
const localAppData = absoluteOnly(env[LOCAL_APP_DATA], paths);
|
|
139
|
+
if (localAppData !== undefined) {
|
|
140
|
+
return paths.join(localAppData, CACHE_DIRECTORY_NAME);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const xdgCacheHome = absoluteOnly(env[XDG_CACHE_HOME], paths);
|
|
145
|
+
if (xdgCacheHome !== undefined) {
|
|
146
|
+
return paths.join(xdgCacheHome, CACHE_DIRECTORY_NAME);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const home = homeOnly(options, paths);
|
|
150
|
+
if (home === undefined) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return paths.join(home, ...HOME_CACHE_SEGMENTS);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the usable cache directory and the cache file inside it, or `undefined` when no safe
|
|
157
|
+
* location exists (no usable environment variable and no absolute home directory, or a relative
|
|
158
|
+
* `cacheDir`). Neither result ever depends on the scanned directory or on `cwd`.
|
|
159
|
+
*/
|
|
160
|
+
function resolveLocation(options) {
|
|
161
|
+
const paths = pathsFor(options.platform);
|
|
162
|
+
const directory = cacheDirectory(options, paths);
|
|
163
|
+
if (directory === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return { directory, file: paths.join(directory, UPDATE_CACHE_FILE_NAME) };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the absolute path of the cache file, or `undefined` when no safe location exists, i.e.
|
|
170
|
+
* when no usable cache environment variable and no absolute home directory is available. A
|
|
171
|
+
* relative `cacheDir` is ignored in favor of those user-scoped defaults. The result never depends
|
|
172
|
+
* on the scanned directory or on `cwd`.
|
|
173
|
+
*
|
|
174
|
+
* @param options - Location inputs (override directory, environment map, home provider, platform).
|
|
175
|
+
* @returns The absolute cache file path, or `undefined` when caching is impossible.
|
|
176
|
+
*/
|
|
177
|
+
export function resolveUpdateCacheFile(options = {}) {
|
|
178
|
+
return resolveLocation(options)?.file;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether the injected clock is usable. A fractional, `NaN`, infinite, negative or absurdly large
|
|
182
|
+
* value cannot establish freshness, so it disables the operation instead of guessing.
|
|
183
|
+
*/
|
|
184
|
+
function isUsableClock(now) {
|
|
185
|
+
return Number.isSafeInteger(now) && now >= 0;
|
|
186
|
+
}
|
|
187
|
+
/** Whether a string can serve as the stored version identifier. See `UNUSABLE_IDENTIFIER`. */
|
|
188
|
+
function isUsableIdentifier(value) {
|
|
189
|
+
return (typeof value === 'string' &&
|
|
190
|
+
value.length > 0 &&
|
|
191
|
+
value.length <= MAX_VERSION_LENGTH &&
|
|
192
|
+
!hasControlCharacter(value) &&
|
|
193
|
+
!UNUSABLE_IDENTIFIER.test(value));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Read at most a bounded amount of text from the cache file.
|
|
197
|
+
*
|
|
198
|
+
* `lstat` (not `stat`) is used deliberately: the entry must be a regular file, so a symlink planted
|
|
199
|
+
* at the cache path - a path this module does not control on shared systems - is a miss, and its
|
|
200
|
+
* target is never read. A directory, an oversized file and any I/O error also end up as `undefined`.
|
|
201
|
+
*/
|
|
202
|
+
async function readCacheText(file) {
|
|
203
|
+
const info = await lstat(file);
|
|
204
|
+
if (!info.isFile() || info.size > MAX_CACHE_BYTES) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const contents = await readFile(file, 'utf8');
|
|
208
|
+
// Decoded length is not byte length, so re-check the bound before parsing anything.
|
|
209
|
+
return contents.length > MAX_CACHE_BYTES ? undefined : contents;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Validate one cached document. The document must be a JSON object carrying exactly `latest` (a
|
|
213
|
+
* usable identifier) and `checkedAt` (a non-negative safe integer); arrays, scalars, invalid JSON,
|
|
214
|
+
* wrong types and unexpected extra keys are all rejected.
|
|
215
|
+
*/
|
|
216
|
+
function parseCacheEntry(contents) {
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(contents);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const record = parsed;
|
|
228
|
+
if (Object.keys(record).length !== 2) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const latest = record['latest'];
|
|
232
|
+
const checkedAt = record['checkedAt'];
|
|
233
|
+
if (!isUsableIdentifier(latest)) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (typeof checkedAt !== 'number' || !Number.isSafeInteger(checkedAt) || checkedAt < 0) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return { latest, checkedAt };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Look up a fresh cached `latest` version.
|
|
243
|
+
*
|
|
244
|
+
* Returns the cached string only when the record exists, is valid, and its age is `>= 0` and below
|
|
245
|
+
* {@link UPDATE_CACHE_TTL_MS}. Every other case - no file, invalid or oversized content, wrong
|
|
246
|
+
* types, an extra key, a negative or future timestamp, a permission error, a symlink, a locked-down
|
|
247
|
+
* home folder - returns `undefined`, i.e. a miss. This function never throws and never logs, so a
|
|
248
|
+
* broken cache can never change a scan.
|
|
249
|
+
*
|
|
250
|
+
* @param options - The injected clock plus the cache location inputs.
|
|
251
|
+
* @returns The cached `latest` version, or `undefined` when the cache must be treated as empty.
|
|
252
|
+
*/
|
|
253
|
+
export async function readCachedLatest(options) {
|
|
254
|
+
try {
|
|
255
|
+
if (!isUsableClock(options.now)) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
const file = resolveUpdateCacheFile(options);
|
|
259
|
+
if (file === undefined) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const text = await readCacheText(file);
|
|
263
|
+
if (text === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
const entry = parseCacheEntry(text);
|
|
267
|
+
if (entry === undefined) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
// A future timestamp means the clock or the file cannot be trusted: refresh, do not guess.
|
|
271
|
+
const age = options.now - entry.checkedAt;
|
|
272
|
+
if (age < 0 || age >= UPDATE_CACHE_TTL_MS) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return entry.latest;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// A missing file (ENOENT) and every other filesystem failure are ordinary cache misses.
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Serialize the whole cache document: exactly the two contract-approved fields, nothing else.
|
|
284
|
+
*/
|
|
285
|
+
function serializeEntry(latest, checkedAt) {
|
|
286
|
+
const entry = { latest, checkedAt };
|
|
287
|
+
return JSON.stringify(entry);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Remove a temporary file without ever reporting a failure.
|
|
291
|
+
*/
|
|
292
|
+
async function removeQuietly(target) {
|
|
293
|
+
try {
|
|
294
|
+
await rm(target, { force: true });
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Nothing to clean up, or the media refuses it; either way the scan is unaffected.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Stage the record in a sibling temporary file and rename it into place, so a concurrent reader -
|
|
302
|
+
* or a crash in the middle of a write - never observes a half-written JSON document. The temporary
|
|
303
|
+
* name carries the process id, so simultaneous DepVitals processes do not share a staging file, and
|
|
304
|
+
* `rename` replaces the previous entry atomically on POSIX and on Windows.
|
|
305
|
+
*
|
|
306
|
+
* All failures are swallowed: a read-only, missing or unwritable cache directory simply means the
|
|
307
|
+
* next run repeats its lookup.
|
|
308
|
+
*/
|
|
309
|
+
async function writeAtomically(file, contents) {
|
|
310
|
+
const temporary = `${file}.${String(process.pid)}.tmp`;
|
|
311
|
+
try {
|
|
312
|
+
await writeFile(temporary, contents, { mode: CACHE_FILE_MODE });
|
|
313
|
+
await rename(temporary, file);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Permission denied, read-only media or a vanished parent directory: skip caching silently.
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
// After a successful rename the temporary name no longer exists, so this is a no-op.
|
|
320
|
+
await removeQuietly(temporary);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Record a `latest` version together with the injected current time.
|
|
325
|
+
*
|
|
326
|
+
* The write is best effort and completely silent: it creates the cache directory if needed
|
|
327
|
+
* (`0o700`), stages the file (`0o600`) and renames it into place, and it resolves normally even when
|
|
328
|
+
* nothing at all could be written. Only the two contract-approved fields are serialized.
|
|
329
|
+
*
|
|
330
|
+
* @param latest - The public `latest` version string to remember; anything whitespace/control-bearing,
|
|
331
|
+
* empty or longer than 64 characters is not written.
|
|
332
|
+
* @param options - The injected clock plus the cache location inputs.
|
|
333
|
+
*/
|
|
334
|
+
export async function writeCachedLatest(latest, options) {
|
|
335
|
+
try {
|
|
336
|
+
if (!isUsableClock(options.now) || !isUsableIdentifier(latest)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const location = resolveLocation(options);
|
|
340
|
+
if (location === undefined) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Creating the directory is part of the best-effort path: a home folder that cannot hold a
|
|
344
|
+
// cache directory simply results in no cache, exactly like a later failing write.
|
|
345
|
+
await mkdir(location.directory, { recursive: true, mode: CACHE_DIRECTORY_MODE });
|
|
346
|
+
await writeAtomically(location.file, serializeEntry(latest, options.now));
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// `mkdir` is the only call here that can still throw; caching is skipped silently.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=updateCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateCache.js","sourceRoot":"","sources":["../../../src/cli/notifier/updateCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single optional update-check orchestrator (Step 67).
|
|
3
|
+
*
|
|
4
|
+
* This module is the only place where the private notifier helpers of Steps 63-66 are combined, and
|
|
5
|
+
* the only place that decides whether a *newer DepVitals version* notice exists for one invocation.
|
|
6
|
+
* The fixed order is the one mandated by the update-notifier contract:
|
|
7
|
+
*
|
|
8
|
+
* 1. eligibility (`isEligibleUpdateCheck`): an ineligible invocation stops here, before any cache
|
|
9
|
+
* read and before any network attempt, so an opt-out, a CI run or a piped stream costs nothing;
|
|
10
|
+
* 2. a fresh cache read (`readCachedLatest`);
|
|
11
|
+
* 3. at most one bounded public lookup on a cache miss (`fetchLatestDepVitalsVersion`);
|
|
12
|
+
* 4. an optional cache write of the validated result;
|
|
13
|
+
* 5. a strict stable-version comparison against the installed CLI version;
|
|
14
|
+
* 6. the notice text, returned to the caller as data.
|
|
15
|
+
*
|
|
16
|
+
* Design rules:
|
|
17
|
+
*
|
|
18
|
+
* - **No output.** The orchestrator never writes to stdout/stderr and never imports a stream. It
|
|
19
|
+
* returns notice text; the CLI entry point owns the channels and the ordering, which is what keeps
|
|
20
|
+
* the deterministic report on stdout byte for byte.
|
|
21
|
+
* - **No project data.** Nothing about the analyzed project (path, manifest, dependency names) is
|
|
22
|
+
* accepted or forwarded here. The inputs are the completed command kind, the installed package
|
|
23
|
+
* version, the environment map and the two interactivity flags.
|
|
24
|
+
* - **Silent by construction.** Every failure path - cache error, transport error, malformed
|
|
25
|
+
* response, timeout, unusable clock, non-stable version - resolves to `undefined`, and the whole
|
|
26
|
+
* pipeline additionally runs inside a `try`/`catch` and inside an explicit wall-clock budget, so a
|
|
27
|
+
* misbehaving cache or registry can never change an exit code and can never delay the process
|
|
28
|
+
* beyond that budget.
|
|
29
|
+
* - **Fully injected.** Clock, cache directory and transport are injectable, which is what lets the
|
|
30
|
+
* CLI tests stay deterministic and offline. With no seams injected the production defaults are the
|
|
31
|
+
* user cache directory and the built-in `fetch`; no test ever reaches the public registry.
|
|
32
|
+
*/
|
|
33
|
+
import { type NotifierCommandKind, type NotifierEnv } from './eligibility.js';
|
|
34
|
+
import { type RegistryFetch } from './registryLookup.js';
|
|
35
|
+
/**
|
|
36
|
+
* The wall-clock budget for the whole optional check (cache read, at most one lookup, cache write).
|
|
37
|
+
*
|
|
38
|
+
* The lookup already carries its own hard bound, so this outer budget only exists to make "the
|
|
39
|
+
* process never waits on an unbounded promise" explicit and verifiable: when it elapses the check
|
|
40
|
+
* resolves to "no notice", whatever the transport is still doing.
|
|
41
|
+
*/
|
|
42
|
+
export declare const UPDATE_CHECK_BUDGET_MS = 1000;
|
|
43
|
+
/** What the check needs to decide, all of it passed in by the CLI entry point. */
|
|
44
|
+
export type UpdateNoticeRequest = {
|
|
45
|
+
/** Which command the CLI completed. Only a completed scan can produce a notice. */
|
|
46
|
+
readonly commandKind: NotifierCommandKind;
|
|
47
|
+
/** The installed DepVitals package version, i.e. the version `package.json` declares. */
|
|
48
|
+
readonly currentVersion: string;
|
|
49
|
+
/** The environment map used for the eligibility gates. */
|
|
50
|
+
readonly env: NotifierEnv;
|
|
51
|
+
/** Whether standard output is an interactive terminal. */
|
|
52
|
+
readonly stdoutTTY: boolean;
|
|
53
|
+
/** Whether standard error is an interactive terminal. */
|
|
54
|
+
readonly stderrTTY: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Test-only seams. They exist so the CLI tests can pin a clock, a temporary cache directory and a
|
|
58
|
+
* stub transport instead of the real cache location and the real network; production passes none.
|
|
59
|
+
*/
|
|
60
|
+
export type UpdateNoticeSeams = {
|
|
61
|
+
/** Cache directory override; only an absolute path is honored (see `updateCache`). */
|
|
62
|
+
readonly cacheDir?: string;
|
|
63
|
+
/** Transport override; defaults to the built-in `fetch`. */
|
|
64
|
+
readonly fetchImpl?: RegistryFetch;
|
|
65
|
+
/** Clock override, in epoch milliseconds; defaults to `Date.now`. */
|
|
66
|
+
readonly now?: () => number;
|
|
67
|
+
/** Budget override, in milliseconds; defaults to {@link UPDATE_CHECK_BUDGET_MS}. */
|
|
68
|
+
readonly budgetMs?: number;
|
|
69
|
+
};
|
|
70
|
+
/** The notifier collaborator the entry point uses; `collectUpdateNotice` is the production default. */
|
|
71
|
+
export type UpdateNoticeCollector = (request: UpdateNoticeRequest) => Promise<string | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* The notice text: two lines, no colour, no emoji, no ANSI, and no claim about how DepVitals was
|
|
74
|
+
* installed. `npx depvitals@latest` is offered as a way to run the published release, not as an
|
|
75
|
+
* instruction to install anything.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatUpdateNotice(currentVersion: string, latestVersion: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Decide whether this invocation has a newer-version notice, and produce its text.
|
|
80
|
+
*
|
|
81
|
+
* @param request - The completed command kind, the installed version, the environment and the two
|
|
82
|
+
* interactivity flags. Nothing project-derived is accepted.
|
|
83
|
+
* @param seams - Test-only clock/cache/transport overrides.
|
|
84
|
+
* @returns The notice text, or `undefined` for every silent case: ineligible invocation, no usable
|
|
85
|
+
* version information, a failed or timed-out lookup, and a `latest` that is not strictly newer.
|
|
86
|
+
*/
|
|
87
|
+
export declare function collectUpdateNotice(request: UpdateNoticeRequest, seams?: UpdateNoticeSeams): Promise<string | undefined>;
|
|
88
|
+
//# sourceMappingURL=updateCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateCheck.d.ts","sourceRoot":"","sources":["../../../src/cli/notifier/updateCheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAC;AAI7B;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,kFAAkF;AAClF,MAAM,MAAM,mBAAmB,GAAG;IAChC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAC1C,yFAAyF;IACzF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC,qEAAqE;IACrE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,uGAAuG;AACvG,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAiBlG;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAKxF;AAsFD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,KAAK,GAAE,iBAAsB,GAC5B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiC7B"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single optional update-check orchestrator (Step 67).
|
|
3
|
+
*
|
|
4
|
+
* This module is the only place where the private notifier helpers of Steps 63-66 are combined, and
|
|
5
|
+
* the only place that decides whether a *newer DepVitals version* notice exists for one invocation.
|
|
6
|
+
* The fixed order is the one mandated by the update-notifier contract:
|
|
7
|
+
*
|
|
8
|
+
* 1. eligibility (`isEligibleUpdateCheck`): an ineligible invocation stops here, before any cache
|
|
9
|
+
* read and before any network attempt, so an opt-out, a CI run or a piped stream costs nothing;
|
|
10
|
+
* 2. a fresh cache read (`readCachedLatest`);
|
|
11
|
+
* 3. at most one bounded public lookup on a cache miss (`fetchLatestDepVitalsVersion`);
|
|
12
|
+
* 4. an optional cache write of the validated result;
|
|
13
|
+
* 5. a strict stable-version comparison against the installed CLI version;
|
|
14
|
+
* 6. the notice text, returned to the caller as data.
|
|
15
|
+
*
|
|
16
|
+
* Design rules:
|
|
17
|
+
*
|
|
18
|
+
* - **No output.** The orchestrator never writes to stdout/stderr and never imports a stream. It
|
|
19
|
+
* returns notice text; the CLI entry point owns the channels and the ordering, which is what keeps
|
|
20
|
+
* the deterministic report on stdout byte for byte.
|
|
21
|
+
* - **No project data.** Nothing about the analyzed project (path, manifest, dependency names) is
|
|
22
|
+
* accepted or forwarded here. The inputs are the completed command kind, the installed package
|
|
23
|
+
* version, the environment map and the two interactivity flags.
|
|
24
|
+
* - **Silent by construction.** Every failure path - cache error, transport error, malformed
|
|
25
|
+
* response, timeout, unusable clock, non-stable version - resolves to `undefined`, and the whole
|
|
26
|
+
* pipeline additionally runs inside a `try`/`catch` and inside an explicit wall-clock budget, so a
|
|
27
|
+
* misbehaving cache or registry can never change an exit code and can never delay the process
|
|
28
|
+
* beyond that budget.
|
|
29
|
+
* - **Fully injected.** Clock, cache directory and transport are injectable, which is what lets the
|
|
30
|
+
* CLI tests stay deterministic and offline. With no seams injected the production defaults are the
|
|
31
|
+
* user cache directory and the built-in `fetch`; no test ever reaches the public registry.
|
|
32
|
+
*/
|
|
33
|
+
import { isEligibleUpdateCheck, } from './eligibility.js';
|
|
34
|
+
import { fetchLatestDepVitalsVersion, } from './registryLookup.js';
|
|
35
|
+
import { readCachedLatest, writeCachedLatest } from './updateCache.js';
|
|
36
|
+
import { isNewerStableVersion } from './versionCompare.js';
|
|
37
|
+
/**
|
|
38
|
+
* The wall-clock budget for the whole optional check (cache read, at most one lookup, cache write).
|
|
39
|
+
*
|
|
40
|
+
* The lookup already carries its own hard bound, so this outer budget only exists to make "the
|
|
41
|
+
* process never waits on an unbounded promise" explicit and verifiable: when it elapses the check
|
|
42
|
+
* resolves to "no notice", whatever the transport is still doing.
|
|
43
|
+
*/
|
|
44
|
+
export const UPDATE_CHECK_BUDGET_MS = 1000;
|
|
45
|
+
/**
|
|
46
|
+
* The production transport: the built-in `fetch`, narrowed to the shape the registry lookup accepts.
|
|
47
|
+
*
|
|
48
|
+
* Only the endpoint and the abort signal are forwarded - never a header, a body, a credential or any
|
|
49
|
+
* project-derived value - because `RegistryRequestInit` has exactly one field.
|
|
50
|
+
*/
|
|
51
|
+
async function builtInFetch(endpoint, init) {
|
|
52
|
+
const response = await fetch(endpoint, { signal: init.signal });
|
|
53
|
+
return { status: response.status, body: response.body };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The notice text: two lines, no colour, no emoji, no ANSI, and no claim about how DepVitals was
|
|
57
|
+
* installed. `npx depvitals@latest` is offered as a way to run the published release, not as an
|
|
58
|
+
* instruction to install anything.
|
|
59
|
+
*/
|
|
60
|
+
export function formatUpdateNotice(currentVersion, latestVersion) {
|
|
61
|
+
return (`\nNew DepVitals version available: ${currentVersion} -> ${latestVersion}\n` +
|
|
62
|
+
'Run: npx depvitals@latest\n');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the `latest` version to compare against, using the cache first.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` when nothing usable is known: a fresh cache hit yields the cached string and
|
|
68
|
+
* no request at all; otherwise exactly one bounded lookup happens, and its validated result is
|
|
69
|
+
* remembered for the next run. A failed lookup yields `undefined` and caches nothing.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveLatestVersion(request, seams) {
|
|
72
|
+
const cacheOptions = {
|
|
73
|
+
env: request.env,
|
|
74
|
+
now: seams.now?.() ?? Date.now(),
|
|
75
|
+
...(seams.cacheDir === undefined ? {} : { cacheDir: seams.cacheDir }),
|
|
76
|
+
};
|
|
77
|
+
const cached = await readCachedLatest(cacheOptions);
|
|
78
|
+
if (cached !== undefined) {
|
|
79
|
+
return cached;
|
|
80
|
+
}
|
|
81
|
+
const fetched = await fetchLatestDepVitalsVersion({
|
|
82
|
+
fetchImpl: seams.fetchImpl ?? builtInFetch,
|
|
83
|
+
});
|
|
84
|
+
if (fetched === null) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
// Best effort and silent: a cache that cannot be written costs the next run one extra lookup.
|
|
88
|
+
await writeCachedLatest(fetched, cacheOptions);
|
|
89
|
+
return fetched;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the whole optional check inside a fixed wall-clock budget.
|
|
93
|
+
*
|
|
94
|
+
* The deadline resolves with the "no notice" value instead of throwing, and it stays **referenced**
|
|
95
|
+
* for exactly as long as the `await` below is pending. That ownership is the point of this helper:
|
|
96
|
+
* the CLI entry point derives its exit status from an awaited call, so if nothing held the event
|
|
97
|
+
* loop open while the notifier was in flight, Node could reach an idle loop with that await still
|
|
98
|
+
* pending and terminate on the unsettled top-level await path - replacing the scan's own `0`/`1`
|
|
99
|
+
* status with exit `13` and a warning. Neither the transport (which may never answer) nor the
|
|
100
|
+
* lookup's `AbortSignal.timeout` (whose timer does not hold the loop open) can make that guarantee,
|
|
101
|
+
* so this timer does.
|
|
102
|
+
*
|
|
103
|
+
* It is bounded and short-lived, never a keepalive: the `finally` clears it on every path, so a
|
|
104
|
+
* cache hit or a fast lookup leaves nothing pending and the process exits immediately. A stalled
|
|
105
|
+
* transport costs at most this budget, and in practice only the lookup's own shorter timeout,
|
|
106
|
+
* because a live event loop is what lets that timeout fire at all.
|
|
107
|
+
*
|
|
108
|
+
* A rejection of the work (which the notifier helpers themselves never produce) propagates to the
|
|
109
|
+
* caller's `catch`, which is also silent.
|
|
110
|
+
*/
|
|
111
|
+
async function withinBudget(work, budgetMs) {
|
|
112
|
+
let timer;
|
|
113
|
+
const deadline = new Promise((resolve) => {
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
resolve(undefined);
|
|
116
|
+
}, budgetMs);
|
|
117
|
+
});
|
|
118
|
+
const task = work();
|
|
119
|
+
// If the deadline wins, a later failure of the work must not become an unhandled rejection in a
|
|
120
|
+
// short-lived CLI process; the answer is already "no notice".
|
|
121
|
+
task.catch(() => undefined);
|
|
122
|
+
try {
|
|
123
|
+
return await Promise.race([task, deadline]);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (timer !== undefined) {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Decide whether this invocation has a newer-version notice, and produce its text.
|
|
133
|
+
*
|
|
134
|
+
* @param request - The completed command kind, the installed version, the environment and the two
|
|
135
|
+
* interactivity flags. Nothing project-derived is accepted.
|
|
136
|
+
* @param seams - Test-only clock/cache/transport overrides.
|
|
137
|
+
* @returns The notice text, or `undefined` for every silent case: ineligible invocation, no usable
|
|
138
|
+
* version information, a failed or timed-out lookup, and a `latest` that is not strictly newer.
|
|
139
|
+
*/
|
|
140
|
+
export async function collectUpdateNotice(request, seams = {}) {
|
|
141
|
+
try {
|
|
142
|
+
// Gate first: an ineligible run must not read the cache, must not touch the network and must not
|
|
143
|
+
// print anything, so every `await` below sits behind this single predicate.
|
|
144
|
+
if (!isEligibleUpdateCheck({
|
|
145
|
+
env: request.env,
|
|
146
|
+
stdoutTTY: request.stdoutTTY,
|
|
147
|
+
stderrTTY: request.stderrTTY,
|
|
148
|
+
commandKind: request.commandKind,
|
|
149
|
+
})) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const latest = await withinBudget(() => resolveLatestVersion(request, seams), seams.budgetMs ?? UPDATE_CHECK_BUDGET_MS);
|
|
153
|
+
if (latest === undefined) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
if (!isNewerStableVersion(request.currentVersion, latest)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return formatUpdateNotice(request.currentVersion, latest);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// The optional notification is never an error source: no message, no exit-code change, no retry.
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=updateCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateCheck.js","sourceRoot":"","sources":["../../../src/cli/notifier/updateCheck.ts"],"names":[],"mappings":"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"}
|