inup 1.6.0 → 1.6.2
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 +1 -1
- package/dist/cli.js +7 -0
- package/dist/config/constants.js +6 -3
- package/dist/core/package-detector.js +20 -0
- package/dist/core/upgrade-runner.js +8 -0
- package/dist/features/debug/index.js +1 -0
- package/dist/features/debug/renderer/performance-modal.js +17 -1
- package/dist/features/debug/services/perf-logger.js +129 -0
- package/dist/features/debug/services/performance-tracker.js +15 -3
- package/dist/features/headless/headless-runner.js +16 -0
- package/dist/services/http/adaptive-controller.js +153 -0
- package/dist/services/http/etag-store.js +91 -0
- package/dist/services/http/resizable-semaphore.js +71 -0
- package/dist/services/http/retry.js +23 -1
- package/dist/services/npm-registry.js +195 -86
- package/dist/utils/debug-logger.js +8 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-env.js +81 -0
- package/package.json +7 -7
- package/dist/services/cache-manager.js +0 -95
- package/dist/services/persistent-cache.js +0 -237
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ to stderr. Exit codes: `0` up to date, `1` updates exist (`--check`), `2` error.
|
|
|
112
112
|
|
|
113
113
|
## Privacy
|
|
114
114
|
|
|
115
|
-
No tracking, no telemetry, no data collection. Package metadata is fetched directly from the npm registry. Download counts come from the npm downloads API.
|
|
115
|
+
No tracking, no telemetry, no data collection. Package metadata is fetched directly from the npm registry. Download counts come from the npm downloads API. Changelog and release notes are fetched from GitHub.
|
|
116
116
|
|
|
117
117
|
## License
|
|
118
118
|
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,10 @@ const config_1 = require("./config");
|
|
|
15
15
|
const utils_1 = require("./utils");
|
|
16
16
|
const git_1 = require("./utils/git");
|
|
17
17
|
const ui_1 = require("./ui");
|
|
18
|
+
// Load developer-only toggles from <inup-repo>/.env.local before anything reads
|
|
19
|
+
// env. Best-effort, gitignored, never overrides real env. Lets perf/debug be
|
|
20
|
+
// "set once" across every project without shell config.
|
|
21
|
+
(0, utils_1.loadInupLocalEnv)();
|
|
18
22
|
const program = new commander_1.Command();
|
|
19
23
|
async function runCli(options) {
|
|
20
24
|
// Resolve colored-output intent before anything renders.
|
|
@@ -88,6 +92,9 @@ async function runCli(options) {
|
|
|
88
92
|
showOptionalDependencyVulnerabilities: projectConfig.showOptionalDependencyVulnerabilities ?? false,
|
|
89
93
|
debug: options.debug || process.env.INUP_DEBUG === '1',
|
|
90
94
|
saveExact: options.saveExact ?? false,
|
|
95
|
+
// Adaptive concurrency defaults ON; it's an internal/dev toggle with no public
|
|
96
|
+
// flag. Set INUP_ADAPTIVE=0 to disable (e.g. for A/B perf comparisons).
|
|
97
|
+
adaptive: process.env.INUP_ADAPTIVE !== '0',
|
|
91
98
|
};
|
|
92
99
|
// Non-interactive (piped / CI / --json / --check) routes to the read-only headless feature;
|
|
93
100
|
// only the interactive path builds the full TUI runner.
|
package/dist/config/constants.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.POOL_CONNECTIONS = exports.REQUEST_TIMEOUT = exports.NPM_REGISTRY_URL = exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
|
|
4
4
|
var package_meta_1 = require("./package-meta");
|
|
5
5
|
Object.defineProperty(exports, "PACKAGE_NAME", { enumerable: true, get: function () { return package_meta_1.PACKAGE_NAME; } });
|
|
6
6
|
Object.defineProperty(exports, "PACKAGE_VERSION", { enumerable: true, get: function () { return package_meta_1.PACKAGE_VERSION; } });
|
|
7
7
|
exports.NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
8
|
-
exports.MAX_CONCURRENT_REQUESTS = 150;
|
|
9
|
-
exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
|
|
10
8
|
exports.REQUEST_TIMEOUT = 60000; // 60 seconds in milliseconds
|
|
9
|
+
// Upper bound for both the undici Pool's connection count and the adaptive
|
|
10
|
+
// concurrency controller's ceiling, kept as one const so they never drift apart.
|
|
11
|
+
// With pipelining:1, effective in-flight requests are capped by the pool's
|
|
12
|
+
// connection count, so the controller must never ramp past it.
|
|
13
|
+
exports.POOL_CONNECTIONS = 24;
|
|
11
14
|
//# sourceMappingURL=constants.js.map
|
|
@@ -55,12 +55,14 @@ class PackageDetector {
|
|
|
55
55
|
maxDepth;
|
|
56
56
|
batchSize = 10;
|
|
57
57
|
maxConcurrency = 10;
|
|
58
|
+
adaptive;
|
|
58
59
|
constructor(options) {
|
|
59
60
|
this.cwd = options?.cwd || process.cwd();
|
|
60
61
|
this.excludePatterns = options?.excludePatterns || [];
|
|
61
62
|
this.scanDirs = options?.scanDirs || [];
|
|
62
63
|
this.ignorePackages = options?.ignorePackages || [];
|
|
63
64
|
this.maxDepth = options?.maxDepth ?? 10;
|
|
65
|
+
this.adaptive = options?.adaptive ?? true;
|
|
64
66
|
this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
|
|
65
67
|
if (this.packageJsonPath) {
|
|
66
68
|
this.packageJson = (0, utils_1.readPackageJson)(this.packageJsonPath);
|
|
@@ -69,6 +71,19 @@ class PackageDetector {
|
|
|
69
71
|
hasPackageJson() {
|
|
70
72
|
return this.packageJsonPath !== null && this.packageJson !== null;
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The resolved fetch configuration for this run, for perf logging. Exposes the
|
|
76
|
+
* exact values handed to the registry fetcher so a logged run is reproducible.
|
|
77
|
+
*/
|
|
78
|
+
getPerfConfig() {
|
|
79
|
+
return {
|
|
80
|
+
cwd: this.cwd,
|
|
81
|
+
adaptive: this.adaptive,
|
|
82
|
+
maxConcurrency: this.maxConcurrency,
|
|
83
|
+
batchSize: this.batchSize,
|
|
84
|
+
poolConnections: config_1.POOL_CONNECTIONS,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
72
87
|
async getOutdatedPackages() {
|
|
73
88
|
const packages = [];
|
|
74
89
|
await this.streamOutdatedPackages((event) => {
|
|
@@ -109,6 +124,11 @@ class PackageDetector {
|
|
|
109
124
|
currentVersions: prepared.currentVersions,
|
|
110
125
|
batchSize: this.batchSize,
|
|
111
126
|
maxConcurrency: this.maxConcurrency,
|
|
127
|
+
adaptive: this.adaptive,
|
|
128
|
+
onControlTick: (tick) => performanceTracker.recordControlTick(tick),
|
|
129
|
+
onPackageTiming: (0, debug_1.isPerfLoggingEnabled)()
|
|
130
|
+
? (name, latencyMs) => performanceTracker.recordPackageTiming({ name, latencyMs })
|
|
131
|
+
: undefined,
|
|
112
132
|
onBatchReady: (batch) => {
|
|
113
133
|
const batchStart = lastBatchEndAt;
|
|
114
134
|
let batchFailedCount = 0;
|
|
@@ -92,6 +92,14 @@ class UpgradeRunner {
|
|
|
92
92
|
progress.isLoading = event.payload.progress.isLoading;
|
|
93
93
|
performanceTracker.mark('firstBatch');
|
|
94
94
|
performanceTracker.mark('allLoaded');
|
|
95
|
+
if ((0, debug_1.isPerfLoggingEnabled)()) {
|
|
96
|
+
(0, debug_1.writePerfLog)({
|
|
97
|
+
...this.detector.getPerfConfig(),
|
|
98
|
+
packageManager: this.packageManager.name,
|
|
99
|
+
mode: 'interactive',
|
|
100
|
+
env: (0, debug_1.perfEnv)(),
|
|
101
|
+
}, performanceTracker.snapshot());
|
|
102
|
+
}
|
|
95
103
|
refreshUI?.();
|
|
96
104
|
}
|
|
97
105
|
});
|
|
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./types/debug.types"), exports);
|
|
18
18
|
__exportStar(require("./services/performance-tracker"), exports);
|
|
19
|
+
__exportStar(require("./services/perf-logger"), exports);
|
|
19
20
|
__exportStar(require("./renderer/performance-modal"), exports);
|
|
20
21
|
//# sourceMappingURL=index.js.map
|
|
@@ -21,7 +21,7 @@ function labelValue(label, value, labelWidth = 22) {
|
|
|
21
21
|
return `${chalk_1.default.white(padded)} ${value}`;
|
|
22
22
|
}
|
|
23
23
|
function buildSections(snapshot) {
|
|
24
|
-
const { phases, counts, batches, failedPackages, packageManager, totalMs } = snapshot;
|
|
24
|
+
const { phases, counts, batches, controlTicks, failedPackages, packageManager, totalMs } = snapshot;
|
|
25
25
|
const pinned = [
|
|
26
26
|
{
|
|
27
27
|
key: 'header',
|
|
@@ -67,6 +67,22 @@ function buildSections(snapshot) {
|
|
|
67
67
|
bodyRows.push(chalk_1.default.gray(' (no batches recorded)'));
|
|
68
68
|
}
|
|
69
69
|
bodyRows.push('');
|
|
70
|
+
bodyRows.push(chalk_1.default.bold('Concurrency'));
|
|
71
|
+
if (controlTicks.length > 0) {
|
|
72
|
+
const limits = controlTicks.map((t) => t.limit);
|
|
73
|
+
const finalTick = controlTicks[controlTicks.length - 1];
|
|
74
|
+
const hardDowns = controlTicks.filter((t) => t.reason === 'hard-down').length;
|
|
75
|
+
bodyRows.push(labelValue('Start limit', formatCount(controlTicks[0].limit)));
|
|
76
|
+
bodyRows.push(labelValue('Peak limit', formatCount(Math.max(...limits))));
|
|
77
|
+
bodyRows.push(labelValue('Final limit', formatCount(finalTick.limit)));
|
|
78
|
+
bodyRows.push(labelValue('Final EWMA', formatMs(finalTick.ewmaMs)));
|
|
79
|
+
bodyRows.push(labelValue('Control ticks', formatCount(controlTicks.length)));
|
|
80
|
+
bodyRows.push(labelValue('Hard back-offs', formatCount(hardDowns)));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
bodyRows.push(chalk_1.default.gray(' (fixed — adaptive off or run too small)'));
|
|
84
|
+
}
|
|
85
|
+
bodyRows.push('');
|
|
70
86
|
bodyRows.push(chalk_1.default.bold('Failures'));
|
|
71
87
|
if (failedPackages.length > 0) {
|
|
72
88
|
for (const name of failedPackages) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPerfLoggingEnabled = isPerfLoggingEnabled;
|
|
4
|
+
exports.perfEnv = perfEnv;
|
|
5
|
+
exports.getPerfDir = getPerfDir;
|
|
6
|
+
exports.writePerfLog = writePerfLog;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const adaptive_controller_1 = require("../../../services/http/adaptive-controller");
|
|
10
|
+
/**
|
|
11
|
+
* Performance debug logger.
|
|
12
|
+
*
|
|
13
|
+
* When INUP_PERF=1, every run writes ONE self-contained JSON file capturing the
|
|
14
|
+
* full configuration plus the performance snapshot (phases, batches, adaptive
|
|
15
|
+
* control ticks, counts). The files accumulate in a gitignored directory so a
|
|
16
|
+
* series of runs can be diffed to find the best-performing configuration.
|
|
17
|
+
*
|
|
18
|
+
* Output location:
|
|
19
|
+
* - Default: `<cwd>/.inup-perf` (logs next to the project being scanned).
|
|
20
|
+
* - INUP_PERF_DIR=<path>: a single centralized directory (e.g. the inup repo)
|
|
21
|
+
* so runs from many different projects collect in one place for analysis.
|
|
22
|
+
* The project name is encoded into each filename so they stay distinct.
|
|
23
|
+
*
|
|
24
|
+
* Each file is independent: it records every input that could affect timing, so
|
|
25
|
+
* a run can be understood (and reproduced) without external context.
|
|
26
|
+
*/
|
|
27
|
+
const PERF_DIR_NAME = '.inup-perf';
|
|
28
|
+
/** Filesystem-safe slug of a project name for use in filenames. */
|
|
29
|
+
function projectSlug(cwd) {
|
|
30
|
+
const name = (0, path_1.basename)((0, path_1.resolve)(cwd)) || 'root';
|
|
31
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40);
|
|
32
|
+
}
|
|
33
|
+
function isPerfLoggingEnabled() {
|
|
34
|
+
return process.env.INUP_PERF === '1';
|
|
35
|
+
}
|
|
36
|
+
/** Capture the env toggles that influence a run, for reproducibility. */
|
|
37
|
+
function perfEnv() {
|
|
38
|
+
return {
|
|
39
|
+
INUP_ADAPTIVE: process.env.INUP_ADAPTIVE,
|
|
40
|
+
INUP_PERF: process.env.INUP_PERF,
|
|
41
|
+
INUP_DEBUG: process.env.INUP_DEBUG,
|
|
42
|
+
CI: process.env.CI,
|
|
43
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve (and lazily create) the perf-log directory.
|
|
48
|
+
*
|
|
49
|
+
* - INUP_PERF_DIR set → that directory verbatim (centralized collection point,
|
|
50
|
+
* e.g. the inup repo's .inup-perf). Relative paths resolve from cwd.
|
|
51
|
+
* - otherwise → `<cwd>/.inup-perf` (logs sit next to the scanned project).
|
|
52
|
+
*/
|
|
53
|
+
function getPerfDir(cwd = process.cwd()) {
|
|
54
|
+
const override = process.env.INUP_PERF_DIR;
|
|
55
|
+
const dir = override
|
|
56
|
+
? (0, path_1.isAbsolute)(override)
|
|
57
|
+
? override
|
|
58
|
+
: (0, path_1.resolve)(cwd, override)
|
|
59
|
+
: (0, path_1.join)(cwd, PERF_DIR_NAME);
|
|
60
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
61
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
66
|
+
function fileStamp(d) {
|
|
67
|
+
return (`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
|
|
68
|
+
`-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}` +
|
|
69
|
+
`-${pad(d.getMilliseconds(), 3)}`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Write one perf record to its own JSON file. Best-effort: never throws into the
|
|
73
|
+
* caller (a debugging aid must not break a real run). Returns the path written,
|
|
74
|
+
* or null if disabled or on failure.
|
|
75
|
+
*/
|
|
76
|
+
function writePerfLog(config, snapshot) {
|
|
77
|
+
if (!isPerfLoggingEnabled())
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const record = {
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
timestamp: now.toISOString(),
|
|
84
|
+
wallMs: snapshot.totalMs,
|
|
85
|
+
config,
|
|
86
|
+
tuning: adaptive_controller_1.DEFAULT_TUNING,
|
|
87
|
+
snapshot,
|
|
88
|
+
};
|
|
89
|
+
const dir = getPerfDir(config.cwd);
|
|
90
|
+
const modeTag = config.mode;
|
|
91
|
+
const adaptiveTag = config.adaptive ? 'adaptive' : 'fixed';
|
|
92
|
+
// Project name in the filename keeps multi-project runs distinct when many
|
|
93
|
+
// collect into one centralized INUP_PERF_DIR.
|
|
94
|
+
const fileName = `run-${fileStamp(now)}-${projectSlug(config.cwd)}-${adaptiveTag}-${modeTag}.json`;
|
|
95
|
+
const filePath = (0, path_1.join)(dir, fileName);
|
|
96
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(record, null, 2));
|
|
97
|
+
// Maintain a stable pointer to the most recent run for quick reads.
|
|
98
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(dir, 'latest.json'), JSON.stringify(record, null, 2));
|
|
99
|
+
// And a cheap one-line-per-run index for fast scanning.
|
|
100
|
+
appendPerfIndex(dir, record);
|
|
101
|
+
if (process.env.INUP_DEBUG === '1') {
|
|
102
|
+
process.stderr.write(`[inup] perf log → ${filePath}\n`);
|
|
103
|
+
}
|
|
104
|
+
return filePath;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Never let perf logging affect the run.
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Append a single line to a NDJSON index for ultra-cheap scanning, best-effort. */
|
|
112
|
+
function appendPerfIndex(dir, record) {
|
|
113
|
+
try {
|
|
114
|
+
const line = JSON.stringify({
|
|
115
|
+
timestamp: record.timestamp,
|
|
116
|
+
wallMs: record.wallMs,
|
|
117
|
+
adaptive: record.config.adaptive,
|
|
118
|
+
mode: record.config.mode,
|
|
119
|
+
uniquePackages: record.snapshot.counts.uniquePackages,
|
|
120
|
+
registryFetch: record.snapshot.phases.registryFetch,
|
|
121
|
+
controlTicks: record.snapshot.controlTicks.length,
|
|
122
|
+
}) + '\n';
|
|
123
|
+
(0, fs_1.appendFileSync)((0, path_1.join)(dir, 'index.ndjson'), line);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* best-effort */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=perf-logger.js.map
|
|
@@ -6,6 +6,8 @@ class PerformanceTracker {
|
|
|
6
6
|
phases = {};
|
|
7
7
|
counts = {};
|
|
8
8
|
batches = [];
|
|
9
|
+
controlTicks = [];
|
|
10
|
+
packageTimings = [];
|
|
9
11
|
failedPackages = [];
|
|
10
12
|
packageManager = null;
|
|
11
13
|
start() {
|
|
@@ -13,6 +15,8 @@ class PerformanceTracker {
|
|
|
13
15
|
this.phases = {};
|
|
14
16
|
this.counts = {};
|
|
15
17
|
this.batches = [];
|
|
18
|
+
this.controlTicks = [];
|
|
19
|
+
this.packageTimings = [];
|
|
16
20
|
this.failedPackages = [];
|
|
17
21
|
this.packageManager = null;
|
|
18
22
|
}
|
|
@@ -30,6 +34,12 @@ class PerformanceTracker {
|
|
|
30
34
|
recordBatch(batch) {
|
|
31
35
|
this.batches.push(batch);
|
|
32
36
|
}
|
|
37
|
+
recordControlTick(tick) {
|
|
38
|
+
this.controlTicks.push(tick);
|
|
39
|
+
}
|
|
40
|
+
recordPackageTiming(timing) {
|
|
41
|
+
this.packageTimings.push(timing);
|
|
42
|
+
}
|
|
33
43
|
recordFailedPackage(name) {
|
|
34
44
|
if (!this.failedPackages.includes(name)) {
|
|
35
45
|
this.failedPackages.push(name);
|
|
@@ -39,15 +49,15 @@ class PerformanceTracker {
|
|
|
39
49
|
this.packageManager = name;
|
|
40
50
|
}
|
|
41
51
|
snapshot() {
|
|
42
|
-
const totalMs = this.startedAt === null
|
|
43
|
-
? null
|
|
44
|
-
: (this.phases.allLoaded ?? Date.now() - this.startedAt);
|
|
52
|
+
const totalMs = this.startedAt === null ? null : (this.phases.allLoaded ?? Date.now() - this.startedAt);
|
|
45
53
|
return {
|
|
46
54
|
startedAt: this.startedAt,
|
|
47
55
|
phases: { ...this.phases },
|
|
48
56
|
totalMs,
|
|
49
57
|
counts: { ...this.counts },
|
|
50
58
|
batches: [...this.batches],
|
|
59
|
+
controlTicks: [...this.controlTicks],
|
|
60
|
+
packageTimings: [...this.packageTimings],
|
|
51
61
|
failedPackages: [...this.failedPackages],
|
|
52
62
|
packageManager: this.packageManager,
|
|
53
63
|
};
|
|
@@ -57,6 +67,8 @@ class PerformanceTracker {
|
|
|
57
67
|
this.phases = {};
|
|
58
68
|
this.counts = {};
|
|
59
69
|
this.batches = [];
|
|
70
|
+
this.controlTicks = [];
|
|
71
|
+
this.packageTimings = [];
|
|
60
72
|
this.failedPackages = [];
|
|
61
73
|
this.packageManager = null;
|
|
62
74
|
}
|
|
@@ -8,6 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const package_detector_1 = require("../../core/package-detector");
|
|
9
9
|
const vulnerability_audit_1 = require("./vulnerability-audit");
|
|
10
10
|
const report_1 = require("./report");
|
|
11
|
+
const debug_1 = require("../../features/debug");
|
|
11
12
|
/**
|
|
12
13
|
* Non-interactive entry point. Resolves the outdated list without rendering the TUI, then either
|
|
13
14
|
* emits a JSON report (--json) or a plain line-based report. With --check, sets a non-zero exit
|
|
@@ -26,8 +27,23 @@ class HeadlessRunner {
|
|
|
26
27
|
if (!this.detector.hasPackageJson()) {
|
|
27
28
|
throw new Error('No package.json found in current directory');
|
|
28
29
|
}
|
|
30
|
+
// Start perf tracking so headless runs produce clean timing data too
|
|
31
|
+
// (the interactive runner starts it itself; headless previously did not).
|
|
32
|
+
const perfEnabled = (0, debug_1.isPerfLoggingEnabled)();
|
|
33
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
34
|
+
if (perfEnabled)
|
|
35
|
+
performanceTracker.start();
|
|
29
36
|
const packages = await this.detector.getOutdatedPackages();
|
|
30
37
|
const outdated = this.detector.getOutdatedPackagesOnly(packages);
|
|
38
|
+
if (perfEnabled) {
|
|
39
|
+
performanceTracker.mark('allLoaded');
|
|
40
|
+
(0, debug_1.writePerfLog)({
|
|
41
|
+
...this.detector.getPerfConfig(),
|
|
42
|
+
packageManager: null,
|
|
43
|
+
mode: 'headless',
|
|
44
|
+
env: (0, debug_1.perfEnv)(),
|
|
45
|
+
}, performanceTracker.snapshot());
|
|
46
|
+
}
|
|
31
47
|
// Audit the current versions (one bulk request, best-effort) and cross-reference each
|
|
32
48
|
// advisory against the upgrade targets, so the report says whether upgrading *fixes* it.
|
|
33
49
|
const vulnerabilities = await (0, vulnerability_audit_1.auditVulnerabilities)(outdated);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AIMD (additive-increase / multiplicative-decrease) concurrency controller.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the congestion-control family TCP uses. It performs no I/O and does not
|
|
6
|
+
* own the semaphore — it is a pure decision function: feed it the outcome of each
|
|
7
|
+
* request, and on each control tick it returns the next concurrency limit, which
|
|
8
|
+
* the caller applies to the resizable semaphore.
|
|
9
|
+
*
|
|
10
|
+
* Back-off signals (only real errors move the limit down):
|
|
11
|
+
* - congestion (HTTP 429/503): the registry explicitly telling us to slow down →
|
|
12
|
+
* immediate hard multiplicative-decrease (×hardDecreaseFactor), the moment the
|
|
13
|
+
* signal arrives, not deferred to the next tick.
|
|
14
|
+
* - retryable/transient errors in a tick window → soft multiplicative-decrease
|
|
15
|
+
* (×softDecreaseFactor) and no increase.
|
|
16
|
+
*
|
|
17
|
+
* Otherwise (healthy link) the limit ramps additively (+increaseStep) toward the
|
|
18
|
+
* ceiling and holds there.
|
|
19
|
+
*
|
|
20
|
+
* NOTE on latency: we sample successful single-attempt latency into an EWMA, but
|
|
21
|
+
* it is used for *instrumentation only* — never to drive back-off. The npm
|
|
22
|
+
* registry is a CDN whose latency varies widely on a perfectly healthy link;
|
|
23
|
+
* reacting to that variance made the controller oscillate and lose to a fixed
|
|
24
|
+
* limit. Real congestion shows up as 429, which we handle directly.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.AdaptiveController = exports.DEFAULT_TUNING = void 0;
|
|
28
|
+
exports.DEFAULT_TUNING = {
|
|
29
|
+
floor: 6,
|
|
30
|
+
ceil: 24,
|
|
31
|
+
increaseStep: 4,
|
|
32
|
+
softDecreaseFactor: 0.7,
|
|
33
|
+
hardDecreaseFactor: 0.5,
|
|
34
|
+
ewmaAlpha: 0.3,
|
|
35
|
+
ticksEveryCompletions: 6,
|
|
36
|
+
};
|
|
37
|
+
const clamp = (value, lo, hi) => Math.max(lo, Math.min(hi, value));
|
|
38
|
+
class AdaptiveController {
|
|
39
|
+
onTick;
|
|
40
|
+
tuning;
|
|
41
|
+
limit;
|
|
42
|
+
// EWMA of successful single-attempt latency. Kept for instrumentation only
|
|
43
|
+
// (the perf modal / logs); it does not drive concurrency decisions.
|
|
44
|
+
ewmaMs = 0;
|
|
45
|
+
hasEwma = false;
|
|
46
|
+
completionsSinceTick = 0;
|
|
47
|
+
retriesSinceTick = 0;
|
|
48
|
+
/**
|
|
49
|
+
* @param packageCount number of packages this run will fetch; drives the smart
|
|
50
|
+
* initial limit so a run starts near its work size and
|
|
51
|
+
* ramps to the ceiling, rather than crawling up from the
|
|
52
|
+
* floor and losing to a fixed baseline.
|
|
53
|
+
* @param onTick optional sink for control decisions (perf modal / logs).
|
|
54
|
+
* @param tuning override the defaults (constants live in one place).
|
|
55
|
+
*/
|
|
56
|
+
constructor(packageCount, onTick, tuning = {}) {
|
|
57
|
+
this.onTick = onTick;
|
|
58
|
+
this.tuning = { ...exports.DEFAULT_TUNING, ...tuning };
|
|
59
|
+
// Smart start: aim at the work size, clamped to [floor, ceil].
|
|
60
|
+
this.limit = clamp(Math.min(this.tuning.ceil, packageCount), this.tuning.floor, this.tuning.ceil);
|
|
61
|
+
}
|
|
62
|
+
/** Whether the controller should even run; tiny runs are better off fixed. */
|
|
63
|
+
static shouldControl(packageCount, tuning = {}) {
|
|
64
|
+
const ceil = tuning.ceil ?? exports.DEFAULT_TUNING.ceil;
|
|
65
|
+
// Nothing to ramp toward and no steady state to learn below the ceiling.
|
|
66
|
+
return packageCount > ceil;
|
|
67
|
+
}
|
|
68
|
+
getLimit() {
|
|
69
|
+
return this.limit;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Record a completed request. Returns a new limit to apply IMMEDIATELY when a
|
|
73
|
+
* congestion signal demands a hard back-off, otherwise null (the limit may
|
|
74
|
+
* still change at the next control tick — see `maybeTick`).
|
|
75
|
+
*/
|
|
76
|
+
record(kind, latencyMs) {
|
|
77
|
+
this.completionsSinceTick++;
|
|
78
|
+
if (kind === 'success' && latencyMs !== undefined) {
|
|
79
|
+
this.sampleLatency(latencyMs);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (kind === 'congested') {
|
|
83
|
+
this.retriesSinceTick++;
|
|
84
|
+
return this.applyHardDecrease();
|
|
85
|
+
}
|
|
86
|
+
// retryable / transient
|
|
87
|
+
this.retriesSinceTick++;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Call after each completion. If a control tick is due, computes and returns
|
|
92
|
+
* the next limit (and emits a ControlTick); otherwise returns null.
|
|
93
|
+
*/
|
|
94
|
+
maybeTick(now = Date.now()) {
|
|
95
|
+
if (this.completionsSinceTick < this.tuning.ticksEveryCompletions) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return this.tick(now);
|
|
99
|
+
}
|
|
100
|
+
sampleLatency(latencyMs) {
|
|
101
|
+
if (!this.hasEwma) {
|
|
102
|
+
this.ewmaMs = latencyMs;
|
|
103
|
+
this.hasEwma = true;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const a = this.tuning.ewmaAlpha;
|
|
107
|
+
this.ewmaMs = a * latencyMs + (1 - a) * this.ewmaMs;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
applyHardDecrease() {
|
|
111
|
+
const next = clamp(Math.round(this.limit * this.tuning.hardDecreaseFactor), this.tuning.floor, this.tuning.ceil);
|
|
112
|
+
this.limit = next;
|
|
113
|
+
this.emit('hard-down');
|
|
114
|
+
// A hard decrease resets the tick window so we don't immediately bump back up.
|
|
115
|
+
this.resetWindow();
|
|
116
|
+
return next;
|
|
117
|
+
}
|
|
118
|
+
tick(now) {
|
|
119
|
+
// Back off only on real errors seen since the last tick; otherwise ramp to
|
|
120
|
+
// the ceiling and hold (latency is not a signal — see the class docblock).
|
|
121
|
+
const retries = this.retriesSinceTick;
|
|
122
|
+
let reason;
|
|
123
|
+
if (retries > 0) {
|
|
124
|
+
this.limit = clamp(Math.round(this.limit * this.tuning.softDecreaseFactor), this.tuning.floor, this.tuning.ceil);
|
|
125
|
+
reason = 'soft-down';
|
|
126
|
+
}
|
|
127
|
+
else if (this.limit < this.tuning.ceil) {
|
|
128
|
+
this.limit = clamp(this.limit + this.tuning.increaseStep, this.tuning.floor, this.tuning.ceil);
|
|
129
|
+
reason = 'up';
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
reason = 'hold';
|
|
133
|
+
}
|
|
134
|
+
this.emit(reason, now);
|
|
135
|
+
this.resetWindow();
|
|
136
|
+
return this.limit;
|
|
137
|
+
}
|
|
138
|
+
emit(reason, now = Date.now()) {
|
|
139
|
+
this.onTick?.({
|
|
140
|
+
atMs: now,
|
|
141
|
+
limit: this.limit,
|
|
142
|
+
ewmaMs: Math.round(this.ewmaMs),
|
|
143
|
+
retries: this.retriesSinceTick,
|
|
144
|
+
reason,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
resetWindow() {
|
|
148
|
+
this.completionsSinceTick = 0;
|
|
149
|
+
this.retriesSinceTick = 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.AdaptiveController = AdaptiveController;
|
|
153
|
+
//# sourceMappingURL=adaptive-controller.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setEtagCacheEnabled = setEtagCacheEnabled;
|
|
4
|
+
exports.readEtag = readEtag;
|
|
5
|
+
exports.writeEtag = writeEtag;
|
|
6
|
+
exports.etagCacheDir = etagCacheDir;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
/** Bump when the on-disk entry shape changes; old generations are ignored. */
|
|
12
|
+
const SCHEMA = 'v1';
|
|
13
|
+
/** Entries untouched for longer than this are swept on first access. */
|
|
14
|
+
const MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
|
|
15
|
+
let enabled = true;
|
|
16
|
+
let sweptThisProcess = false;
|
|
17
|
+
/** Allow tests/callers to toggle the store without touching call sites. */
|
|
18
|
+
function setEtagCacheEnabled(value) {
|
|
19
|
+
enabled = value;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve (and lazily create) the cache directory, sweeping stale entries once. */
|
|
22
|
+
function cacheDir() {
|
|
23
|
+
// Version data is not project-specific, so it lives in the OS temp area, shared
|
|
24
|
+
// across every project the user scans.
|
|
25
|
+
const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'inup', 'etag-cache', SCHEMA);
|
|
26
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
27
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
if (!sweptThisProcess) {
|
|
30
|
+
sweptThisProcess = true;
|
|
31
|
+
sweepStale(dir);
|
|
32
|
+
}
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
/** Delete entries not modified within MAX_AGE_MS. Best-effort, runs once. */
|
|
36
|
+
function sweepStale(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
39
|
+
for (const name of (0, node_fs_1.readdirSync)(dir)) {
|
|
40
|
+
const file = (0, node_path_1.join)(dir, name);
|
|
41
|
+
try {
|
|
42
|
+
if ((0, node_fs_1.statSync)(file).mtimeMs < cutoff)
|
|
43
|
+
(0, node_fs_1.unlinkSync)(file);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* skip files we can't stat/remove */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* sweeping is optional; never fail a run over it */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Stable, filesystem-safe filename for a cache key (the registry path). */
|
|
55
|
+
function fileFor(key) {
|
|
56
|
+
const hash = (0, node_crypto_1.createHash)('sha1').update(key).digest('hex');
|
|
57
|
+
return (0, node_path_1.join)(cacheDir(), `${hash}.json`);
|
|
58
|
+
}
|
|
59
|
+
/** Read a stored entry, or null if absent/unreadable/disabled. */
|
|
60
|
+
function readEtag(key) {
|
|
61
|
+
if (!enabled)
|
|
62
|
+
return null;
|
|
63
|
+
try {
|
|
64
|
+
const file = fileFor(key);
|
|
65
|
+
if (!(0, node_fs_1.existsSync)(file))
|
|
66
|
+
return null;
|
|
67
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
68
|
+
if (!parsed || typeof parsed.etag !== 'string' || !parsed.data)
|
|
69
|
+
return null;
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Persist an entry. Best-effort; failures are ignored. */
|
|
77
|
+
function writeEtag(key, etag, data) {
|
|
78
|
+
if (!enabled || !etag)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
(0, node_fs_1.writeFileSync)(fileFor(key), JSON.stringify({ etag, data }));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* best-effort */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Test/maintenance helper: the resolved cache directory. */
|
|
88
|
+
function etagCacheDir() {
|
|
89
|
+
return cacheDir();
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=etag-store.js.map
|