inup 1.6.0 → 1.6.3
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 +81 -4
- package/dist/cli.js +23 -3
- 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/core/upgrader.js +36 -15
- 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 +89 -5
- 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/services/package-manager-detector.js +4 -0
- package/dist/utils/debug-logger.js +8 -1
- package/dist/utils/filesystem/scan.js +13 -2
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-env.js +81 -0
- package/package.json +7 -10
- package/dist/services/cache-manager.js +0 -95
- package/dist/services/persistent-cache.js +0 -237
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ inup [options]
|
|
|
51
51
|
--package-manager <name> Force package manager (npm, yarn, pnpm, bun)
|
|
52
52
|
--json Print a machine-readable JSON report and exit (read-only)
|
|
53
53
|
-c, --check Exit non-zero if updates exist, without writing (for CI)
|
|
54
|
+
--apply Non-interactively write upgrades + install (for CI/automation)
|
|
55
|
+
--target <level> With --apply: how far to bump — minor (default) | patch | latest
|
|
54
56
|
--debug Write verbose debug logs
|
|
55
57
|
```
|
|
56
58
|
|
|
@@ -61,11 +63,19 @@ pipeline waiting on the interactive UI. Both `--json` and `--check` are **read-o
|
|
|
61
63
|
they never edit `package.json` or install.
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
|
-
inup --check
|
|
65
|
-
inup --json | jq
|
|
66
|
-
inup | cat
|
|
66
|
+
inup --check # exit 1 if anything is outdated → fails the build
|
|
67
|
+
inup --json | jq # structured drift report for dashboards/bots
|
|
68
|
+
inup | cat # plain line-based report when piped to a log
|
|
69
|
+
inup --apply # write safe in-range bumps + install (non-interactive)
|
|
70
|
+
inup --apply --target latest # include major bumps; --json to also emit the report
|
|
67
71
|
```
|
|
68
72
|
|
|
73
|
+
Unlike `--json` and `--check`, **`--apply` writes**: it bumps `package.json` and runs your package
|
|
74
|
+
manager's install to update the lockfile. By default (`--target minor`) it only applies **in-range**
|
|
75
|
+
updates and leaves majors for you to review; `--target latest` includes majors. It honors `.inuprc`
|
|
76
|
+
(`ignore`, `exclude`, `scanDirs`) exactly as the report does — a package the config excludes is
|
|
77
|
+
never written. With `--apply --json`, the install output goes to stderr so stdout stays pure JSON.
|
|
78
|
+
|
|
69
79
|
Each reported package carries its health signals: `deprecated` (npm deprecation message), `enginesNode`
|
|
70
80
|
(declared `engines.node`), and `vulnerability` (known advisories on the currently-installed version,
|
|
71
81
|
from one bulk `npm audit`-style request). Every advisory is **cross-referenced against the upgrade
|
|
@@ -81,6 +91,73 @@ agents can pin to a known shape.
|
|
|
81
91
|
Output hygiene: with `--json`, stdout carries **only** the JSON document; all progress and warnings go
|
|
82
92
|
to stderr. Exit codes: `0` up to date, `1` updates exist (`--check`), `2` error.
|
|
83
93
|
|
|
94
|
+
## GitHub Action — one rolling upgrade PR
|
|
95
|
+
|
|
96
|
+
Run inup on a schedule and get **one rolling pull request** with safe upgrades applied. Re-runs update
|
|
97
|
+
the same PR instead of opening new ones.
|
|
98
|
+
|
|
99
|
+
Add this workflow to **your** repo:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
# .github/workflows/inup.yml
|
|
103
|
+
name: inup
|
|
104
|
+
on:
|
|
105
|
+
schedule:
|
|
106
|
+
- cron: '0 6 * * *' # daily at 06:00 UTC
|
|
107
|
+
workflow_dispatch: {}
|
|
108
|
+
|
|
109
|
+
permissions:
|
|
110
|
+
contents: write
|
|
111
|
+
pull-requests: write
|
|
112
|
+
|
|
113
|
+
jobs:
|
|
114
|
+
upgrade:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: donfear/inup@v1
|
|
119
|
+
with:
|
|
120
|
+
target: minor # minor (default) | patch | latest
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That's it. Also enable Settings → Actions → General → **Workflow permissions** →
|
|
124
|
+
**"Allow GitHub Actions to create and approve pull requests"** so the action can open the PR.
|
|
125
|
+
|
|
126
|
+
### Commit as you, not the bot
|
|
127
|
+
|
|
128
|
+
By default the upgrade commit is authored by `github-actions[bot]`. To attribute it to **you**, store
|
|
129
|
+
your name/email as repo secrets (Settings → Secrets and variables → Actions) and pass `committer`/`author`:
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
- uses: donfear/inup@v1
|
|
133
|
+
with:
|
|
134
|
+
committer: ${{ secrets.GH_USERNAME }} <${{ secrets.GH_EMAIL }}>
|
|
135
|
+
author: ${{ secrets.GH_USERNAME }} <${{ secrets.GH_EMAIL }}>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### All inputs
|
|
139
|
+
|
|
140
|
+
| Input | Default | Description |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `target` | `minor` | How far to bump: `minor` (in-range), `patch`, or `latest` (includes majors). |
|
|
143
|
+
| `directory` | `.` | Directory to run in. |
|
|
144
|
+
| `package-manager` | _(auto)_ | Force `npm`/`yarn`/`pnpm`/`bun`; empty auto-detects from the lockfile. |
|
|
145
|
+
| `node-version` | `22` | Node.js version for the run (minimum `22.19`). |
|
|
146
|
+
| `inup-version` | `latest` | inup version to run (pin for reproducible runs). |
|
|
147
|
+
| `pr-branch` | `inup/dependency-upgrades` | Branch for the rolling PR. |
|
|
148
|
+
| `pr-title` | `chore(deps): dependency upgrades` | PR title. |
|
|
149
|
+
| `commit-message` | `chore(deps): upgrade dependencies via inup` | Commit message. |
|
|
150
|
+
| `base` | _(default branch)_ | Base branch the PR targets. |
|
|
151
|
+
| `labels` | `dependencies` | Labels to apply to the PR. |
|
|
152
|
+
| `token` | `${{ github.token }}` | Token to push + open the PR. Pass a PAT to also trigger CI on the PR. |
|
|
153
|
+
| `committer` | `github-actions[bot]` | Commit committer, as `Name <email>`. |
|
|
154
|
+
| `author` | _(triggering user)_ | Commit author, as `Name <email>`. |
|
|
155
|
+
|
|
156
|
+
Outputs: `outdated`, `vulnerable`, `pull-request-number`.
|
|
157
|
+
|
|
158
|
+
> `@v1` floats to the latest `1.x` release; pin to `@v1.6.2` or a SHA for reproducible runs. inup
|
|
159
|
+
> honors your `.inuprc` (`ignore`, `exclude`, `scanDirs`).
|
|
160
|
+
|
|
84
161
|
## Keyboard Shortcuts
|
|
85
162
|
|
|
86
163
|
<!-- KEYS:START -->
|
|
@@ -112,7 +189,7 @@ to stderr. Exit codes: `0` up to date, `1` updates exist (`--check`), `2` error.
|
|
|
112
189
|
|
|
113
190
|
## Privacy
|
|
114
191
|
|
|
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.
|
|
192
|
+
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
193
|
|
|
117
194
|
## License
|
|
118
195
|
|
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.
|
|
@@ -24,8 +28,14 @@ async function runCli(options) {
|
|
|
24
28
|
(0, utils_1.enableDebugLogging)();
|
|
25
29
|
}
|
|
26
30
|
// Headless when piped, in CI, or when a non-interactive flag is set. The TUI only renders in
|
|
27
|
-
// interactive mode; everything else routes through the read-only
|
|
28
|
-
const interactive = !!process.stdout.isTTY && !process.env.CI && !options.json && !options.check;
|
|
31
|
+
// interactive mode; everything else routes through the headless path (read-only, unless --apply).
|
|
32
|
+
const interactive = !!process.stdout.isTTY && !process.env.CI && !options.json && !options.check && !options.apply;
|
|
33
|
+
// Validate --target early so a typo fails fast instead of silently defaulting.
|
|
34
|
+
if (options.target && !['minor', 'patch', 'latest'].includes(options.target)) {
|
|
35
|
+
console.error(chalk_1.default.red(`Invalid target: ${options.target}`));
|
|
36
|
+
console.error(chalk_1.default.yellow('Valid options: minor, patch, latest'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
29
39
|
// The dirty-tree prompt would hang without a TTY; headless is read-only anyway, so skip it.
|
|
30
40
|
if (interactive) {
|
|
31
41
|
const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
|
|
@@ -88,11 +98,19 @@ async function runCli(options) {
|
|
|
88
98
|
showOptionalDependencyVulnerabilities: projectConfig.showOptionalDependencyVulnerabilities ?? false,
|
|
89
99
|
debug: options.debug || process.env.INUP_DEBUG === '1',
|
|
90
100
|
saveExact: options.saveExact ?? false,
|
|
101
|
+
// Adaptive concurrency defaults ON; it's an internal/dev toggle with no public
|
|
102
|
+
// flag. Set INUP_ADAPTIVE=0 to disable (e.g. for A/B perf comparisons).
|
|
103
|
+
adaptive: process.env.INUP_ADAPTIVE !== '0',
|
|
91
104
|
};
|
|
92
105
|
// Non-interactive (piped / CI / --json / --check) routes to the read-only headless feature;
|
|
93
106
|
// only the interactive path builds the full TUI runner.
|
|
94
107
|
if (!interactive) {
|
|
95
|
-
await new headless_1.HeadlessRunner(runnerOptions).run({
|
|
108
|
+
await new headless_1.HeadlessRunner(runnerOptions).run({
|
|
109
|
+
json: options.json,
|
|
110
|
+
check: options.check,
|
|
111
|
+
apply: options.apply,
|
|
112
|
+
target: options.target || 'minor',
|
|
113
|
+
});
|
|
96
114
|
return;
|
|
97
115
|
}
|
|
98
116
|
await new index_1.UpgradeRunner(runnerOptions).run();
|
|
@@ -133,6 +151,8 @@ program
|
|
|
133
151
|
.option('--save-exact', 'write exact versions instead of preserving the range prefix (^/~)')
|
|
134
152
|
.option('--json', 'print a machine-readable JSON report and exit (non-interactive, read-only)')
|
|
135
153
|
.option('-c, --check', 'exit non-zero if updates exist, without writing (for CI; read-only)')
|
|
154
|
+
.option('--apply', 'non-interactively write upgrades to package.json and run install (honors .inuprc ignore/exclude)')
|
|
155
|
+
.option('--target <level>', 'with --apply: how far to bump — minor | patch | latest (default: minor, in-range only)', 'minor')
|
|
136
156
|
.action(runCli);
|
|
137
157
|
// Handle uncaught errors gracefully
|
|
138
158
|
process.on('uncaughtException', (error) => {
|
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
|
});
|
package/dist/core/upgrader.js
CHANGED
|
@@ -12,12 +12,17 @@ const child_process_1 = require("child_process");
|
|
|
12
12
|
const utils_1 = require("../utils");
|
|
13
13
|
class PackageUpgrader {
|
|
14
14
|
packageManager;
|
|
15
|
-
|
|
15
|
+
quiet;
|
|
16
|
+
/** When quiet, send our own progress to stderr so stdout stays reserved for the JSON document. */
|
|
17
|
+
log;
|
|
18
|
+
constructor(packageManager, options) {
|
|
16
19
|
this.packageManager = packageManager;
|
|
20
|
+
this.quiet = options?.quiet ?? false;
|
|
21
|
+
this.log = this.quiet ? (msg) => console.error(msg) : (msg) => console.log(msg);
|
|
17
22
|
}
|
|
18
23
|
async upgradePackages(choices, _packageInfos) {
|
|
19
24
|
if (choices.length === 0) {
|
|
20
|
-
|
|
25
|
+
this.log(chalk_1.default.yellow('No packages to upgrade.'));
|
|
21
26
|
return;
|
|
22
27
|
}
|
|
23
28
|
// Group choices by package.json path and dependency type
|
|
@@ -26,12 +31,12 @@ class PackageUpgrader {
|
|
|
26
31
|
if (choiceList.length === 0)
|
|
27
32
|
continue;
|
|
28
33
|
const [packageJsonPath, type] = fileAndType.split('|');
|
|
29
|
-
|
|
34
|
+
this.log(`Processing ${type} in ${packageJsonPath}`);
|
|
30
35
|
await this.upgradeChoiceGroup(choiceList, packageJsonPath, type);
|
|
31
36
|
}
|
|
32
37
|
// Count unique packages upgraded
|
|
33
38
|
const uniquePackages = new Set(choices.map((c) => c.name));
|
|
34
|
-
|
|
39
|
+
this.log(chalk_1.default.green(`\n✅ Successfully upgraded ${uniquePackages.size} package(s)!`));
|
|
35
40
|
// Execute package manager install after all upgrades are complete
|
|
36
41
|
await this.runInstall(choices);
|
|
37
42
|
}
|
|
@@ -50,16 +55,23 @@ class PackageUpgrader {
|
|
|
50
55
|
(0, utils_1.executeCommand)(`${this.packageManager.name} --version`, installDir);
|
|
51
56
|
}
|
|
52
57
|
catch (error) {
|
|
53
|
-
|
|
58
|
+
this.log(chalk_1.default.yellow(`\n⚠️ ${this.packageManager.displayName} is detected but not installed on your system.\n` +
|
|
54
59
|
`Please run the install command manually:\n` +
|
|
55
60
|
` cd ${installDir}\n` +
|
|
56
61
|
` ${this.packageManager.installCommand}\n`));
|
|
57
62
|
return; // Skip install, let user do it manually
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
// We just rewrote package.json, so the install must be allowed to regenerate the lockfile.
|
|
65
|
+
// pnpm/yarn default to frozen/immutable installs under CI; writeInstallCommand opts out.
|
|
66
|
+
const installCommand = this.packageManager.writeInstallCommand ?? this.packageManager.installCommand;
|
|
67
|
+
this.log(chalk_1.default.cyan(`\n📦 Running ${installCommand}...\n`));
|
|
68
|
+
// In quiet mode, send the install child's stdout to *our* stderr (fd 2). The child uses
|
|
69
|
+
// inherited fds, so its progress output bypasses any JS shim — redirecting at spawn time is
|
|
70
|
+
// the only reliable way to keep stdout reserved for the --json document. stderr stays inherited.
|
|
71
|
+
const stdio = this.quiet ? ['inherit', 2, 'inherit'] : 'inherit';
|
|
72
|
+
const result = (0, child_process_1.spawnSync)(installCommand, {
|
|
61
73
|
cwd: installDir,
|
|
62
|
-
stdio
|
|
74
|
+
stdio,
|
|
63
75
|
shell: true,
|
|
64
76
|
});
|
|
65
77
|
if (result.error) {
|
|
@@ -67,9 +79,9 @@ class PackageUpgrader {
|
|
|
67
79
|
}
|
|
68
80
|
if (result.status !== 0) {
|
|
69
81
|
if (result.signal) {
|
|
70
|
-
throw new Error(`${
|
|
82
|
+
throw new Error(`${installCommand} terminated by signal ${result.signal}`);
|
|
71
83
|
}
|
|
72
|
-
throw new Error(`${
|
|
84
|
+
throw new Error(`${installCommand} exited with code ${result.status}`);
|
|
73
85
|
}
|
|
74
86
|
}
|
|
75
87
|
groupChoicesByFileAndType(choices) {
|
|
@@ -86,11 +98,14 @@ class PackageUpgrader {
|
|
|
86
98
|
async upgradeChoiceGroup(choices, packageJsonPath, type) {
|
|
87
99
|
// Validate that package.json exists
|
|
88
100
|
if (!(0, fs_1.existsSync)(packageJsonPath)) {
|
|
89
|
-
|
|
101
|
+
this.log(chalk_1.default.yellow(`⚠️ Skipping ${type} in ${packageJsonPath} - package.json file not found`));
|
|
90
102
|
return;
|
|
91
103
|
}
|
|
92
104
|
const packageDir = packageJsonPath.replace('/package.json', '');
|
|
93
|
-
|
|
105
|
+
// The spinner animates on stdout; skip it in quiet mode so the --json document stays clean.
|
|
106
|
+
const spinner = this.quiet
|
|
107
|
+
? null
|
|
108
|
+
: (0, nanospinner_1.createSpinner)(`Upgrading ${type} in ${packageDir}...`).start();
|
|
94
109
|
try {
|
|
95
110
|
// Read the current package.json — keep the raw text so we can round-trip its formatting
|
|
96
111
|
const rawContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
|
|
@@ -128,15 +143,21 @@ class PackageUpgrader {
|
|
|
128
143
|
(0, fs_1.writeFileSync)(packageJsonPath, nextContent);
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
|
-
spinner
|
|
146
|
+
if (spinner)
|
|
147
|
+
spinner.success({ text: `Upgraded ${choices.length} ${type} in ${packageDir}` });
|
|
148
|
+
else
|
|
149
|
+
this.log(chalk_1.default.green(`✔ Upgraded ${choices.length} ${type} in ${packageDir}`));
|
|
132
150
|
// Show which packages were upgraded
|
|
133
151
|
choices.forEach((choice) => {
|
|
134
152
|
const upgradeTypeColor = choice.upgradeType === 'range' ? chalk_1.default.yellow : chalk_1.default.red;
|
|
135
|
-
|
|
153
|
+
this.log(` ${chalk_1.default.green('✓')} ${chalk_1.default.cyan(choice.name)} → ${upgradeTypeColor(choice.targetVersion)}`);
|
|
136
154
|
});
|
|
137
155
|
}
|
|
138
156
|
catch (error) {
|
|
139
|
-
spinner
|
|
157
|
+
if (spinner)
|
|
158
|
+
spinner.error({ text: `Failed to upgrade ${type} in ${packageDir}` });
|
|
159
|
+
else
|
|
160
|
+
this.log(chalk_1.default.red(`✖ Failed to upgrade ${type} in ${packageDir}`));
|
|
140
161
|
console.error(chalk_1.default.red(`Error: ${error}`));
|
|
141
162
|
throw error;
|
|
142
163
|
}
|
|
@@ -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
|
}
|