inup 1.5.0 → 1.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Donfear
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,49 +1,45 @@
1
- # 🚀 inup
1
+ # inup — Interactive Dependency Upgrader
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/inup?logo=npm&logoColor=%23CB3837&style=for-the-badge&color=crimson)](https://www.npmjs.com/package/inup)
4
4
  [![Downloads](https://img.shields.io/npm/dm/inup?style=for-the-badge&color=646CFF&logoColor=white)](https://www.npmjs.com/package/inup)
5
5
  [![Total downloads](https://img.shields.io/npm/dt/inup?style=for-the-badge&color=informational)](https://www.npmjs.com/package/inup)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/donfear/inup/ci.yml?branch=main&style=for-the-badge&label=CI)](https://github.com/donfear/inup/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://github.com/donfear/inup/blob/main/LICENSE)
6
8
 
7
- Upgrade your dependencies interactively. Works with npm, yarn, pnpm, and bun.
9
+ Interactively upgrade outdated dependencies across npm, yarn, pnpm, and bun. Auto-detects your package manager, works in monorepos and workspaces, and requires zero configuration.
8
10
 
9
11
  ![Interactive Upgrade Demo](docs/demo/interactive-upgrade.gif)
10
12
 
11
- ## 🚀 Usage
13
+ ## Quick Start
12
14
 
13
15
  ```bash
14
16
  npx inup
15
17
  ```
16
18
 
17
- Or install globally:
19
+ Or install globally with your preferred package manager:
18
20
 
19
21
  ```bash
20
22
  npm install -g inup
23
+ pnpm add -g inup
24
+ yarn global add inup
25
+ bun add -g inup
21
26
  ```
22
27
 
23
- That's it. The tool scans your project, finds outdated packages, and lets you pick what to upgrade.
28
+ Run `inup` in any project it scans for outdated packages and lets you pick what to upgrade.
24
29
 
25
- ## 💡 Why inup?
30
+ ## Why inup?
26
31
 
27
- - **Inclusive by Default**: We load Dev, Peer, and Optional dependencies automatically. No more restarting the tool because you forgot a `--peer` flag.
28
- - **Live Toggles**: Toggle dependency types (`d`, `p`, `o`) on the fly without exiting.
29
- - **Zero Config**: Auto-detects your package manager.
30
- - **Monorepo Ready**: Seamlessly handles workspaces.
31
- - **Modern UX**: Search with `/`, view package details with `i`, and swap themes with `t`.
32
+ - **All Dependencies at Once** Dev, peer, and optional dependencies load automatically. No more re-running with `--peer` or `--dev` flags.
33
+ - **Live Toggles** Filter dependency types (`d`, `p`, `o`) on the fly without restarting.
34
+ - **Zero Config** Auto-detects npm, yarn, pnpm, or bun from your lockfile.
35
+ - **Monorepo Ready** Discovers and upgrades across workspaces seamlessly.
36
+ - **Vulnerability Audit** Flags known security vulnerabilities right in the package list so you know what's risky before upgrading.
37
+ - **Changelog Viewer** — Read release notes and changelogs inline without leaving the terminal.
38
+ - **Built-in Search** — Press `/` to filter packages instantly.
39
+ - **Package Details** — Press `i` to view package info, download stats, and more.
40
+ - **Themes** — Press `t` to switch between color themes.
32
41
 
33
- ## ⌨️ Keyboard Shortcuts
34
-
35
- - `↑/↓` - Navigate packages
36
- - `←/→` - Select version (current, patch, minor, major)
37
- - `Space` - Toggle selection
38
- - `m` - Select all minor updates
39
- - `l` - Select all latest updates
40
- - `u` - Unselect all
41
- - `/` - Search packages
42
- - `t` - Change theme
43
- - `i` - View package info
44
- - `Enter` - Confirm and upgrade
45
-
46
- ## ⚙️ Options
42
+ ## Options
47
43
 
48
44
  ```bash
49
45
  inup [options]
@@ -56,12 +52,25 @@ inup [options]
56
52
  --debug Write verbose debug logs
57
53
  ```
58
54
 
59
- ## 🔒 Privacy
55
+ ## Keyboard Shortcuts
56
+
57
+ | Key | Action |
58
+ |-----|--------|
59
+ | `↑` `↓` | Navigate packages |
60
+ | `←` `→` | Select version (current, patch, minor, major) |
61
+ | `Space` | Toggle selection |
62
+ | `m` | Select all minor updates |
63
+ | `l` | Select all latest updates |
64
+ | `u` | Unselect all |
65
+ | `/` | Search packages |
66
+ | `i` | View package info |
67
+ | `t` | Change theme |
68
+ | `Enter` | Confirm and upgrade |
60
69
 
61
- We don't track anything. Ever.
70
+ ## Privacy
62
71
 
63
- Version checks and package metadata are fetched from the npm registry. When needed for immutable exact-version manifests, inup may also fetch a pinned `package.json` from jsDelivr. Weekly download counts come from the npm downloads API.
72
+ No tracking, no telemetry, no data collection. Package metadata is fetched directly from the npm registry. Download counts come from the npm downloads API. When needed for exact-version manifests, inup may fetch a pinned `package.json` from jsDelivr.
64
73
 
65
- ## 📄 License
74
+ ## License
66
75
 
67
- MIT
76
+ [MIT](LICENSE)
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.runCli = runCli;
7
8
  const commander_1 = require("commander");
8
9
  const chalk_1 = __importDefault(require("chalk"));
9
10
  const fs_1 = require("fs");
@@ -12,23 +13,23 @@ const index_1 = require("./index");
12
13
  const services_1 = require("./services");
13
14
  const config_1 = require("./config");
14
15
  const utils_1 = require("./utils");
16
+ const git_1 = require("./utils/git");
17
+ const terminal_input_1 = require("./ui/utils/terminal-input");
15
18
  const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../package.json'), 'utf-8'));
16
19
  const program = new commander_1.Command();
17
- program
18
- .name('inup')
19
- .description('Interactive upgrade tool for package managers. Auto-detects and works with npm, yarn, pnpm, and bun.')
20
- .version(packageJson.version)
21
- .option('-d, --dir <directory>', 'specify directory to run in', process.cwd())
22
- .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
23
- .option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
24
- .option('--max-depth <number>', 'maximum directory depth for package.json discovery', '10')
25
- .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
26
- .option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
27
- .action(async (options) => {
20
+ async function runCli(options) {
28
21
  const cwd = (0, path_1.resolve)(options.dir);
29
22
  if (options.debug || process.env.INUP_DEBUG === '1') {
30
23
  (0, utils_1.enableDebugLogging)();
31
24
  }
25
+ const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
26
+ if (gitState.isRepo && gitState.isDirty) {
27
+ const shouldProceed = await terminal_input_1.TerminalInput.promptForImmediateConfirmation(`${chalk_1.default.yellow('Warning:')} dirty working tree. Proceed anyway? ${chalk_1.default.dim('[y/N]')} `, false);
28
+ if (!shouldProceed) {
29
+ console.log(chalk_1.default.yellow('Upgrade cancelled.'));
30
+ return;
31
+ }
32
+ }
32
33
  // Load project config from .inuprc
33
34
  const projectConfig = (0, config_1.loadProjectConfig)(cwd);
34
35
  // Merge CLI exclude patterns with config
@@ -99,7 +100,18 @@ program
99
100
  console.log(chalk_1.default.yellow('└' + '─'.repeat(78) + '┘'));
100
101
  console.log('');
101
102
  }
102
- });
103
+ }
104
+ program
105
+ .name('inup')
106
+ .description('Interactive upgrade tool for package managers. Auto-detects and works with npm, yarn, pnpm, and bun.')
107
+ .version(packageJson.version)
108
+ .option('-d, --dir <directory>', 'specify directory to run in', process.cwd())
109
+ .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
110
+ .option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
111
+ .option('--max-depth <number>', 'maximum directory depth for package.json discovery', '10')
112
+ .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
113
+ .option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
114
+ .action(runCli);
103
115
  // Handle uncaught errors gracefully
104
116
  process.on('uncaughtException', (error) => {
105
117
  console.error(chalk_1.default.red('Uncaught Exception:'), error.message);
@@ -128,5 +140,7 @@ process.on('SIGTERM', () => {
128
140
  console.log(chalk_1.default.yellow('\n\nOperation cancelled.'));
129
141
  process.exit(0);
130
142
  });
131
- program.parse();
143
+ if (require.main === module) {
144
+ program.parse();
145
+ }
132
146
  //# sourceMappingURL=cli.js.map
@@ -40,12 +40,13 @@ const services_1 = require("../services");
40
40
  const config_1 = require("../config");
41
41
  const utils_2 = require("../ui/utils");
42
42
  const utils_3 = require("../utils");
43
+ const debug_1 = require("../features/debug");
43
44
  class PackageDetector {
44
45
  constructor(options) {
45
46
  this.packageJsonPath = null;
46
47
  this.packageJson = null;
47
48
  this.batchSize = 25;
48
- this.batchConcurrency = 5;
49
+ this.batchConcurrency = 25;
49
50
  this.cwd = options?.cwd || process.cwd();
50
51
  this.excludePatterns = options?.excludePatterns || [];
51
52
  this.ignorePackages = options?.ignorePackages || [];
@@ -89,9 +90,14 @@ class PackageDetector {
89
90
  const packageLookup = new Map();
90
91
  let resolved = 0;
91
92
  let failed = 0;
93
+ const performanceTracker = (0, debug_1.getPerformanceTracker)();
94
+ let batchIndex = 0;
95
+ let lastBatchEndAt = Date.now();
92
96
  const tFetch = Date.now();
93
97
  utils_3.debugLog.info('PackageDetector', 'fetching version data via npm registry in batches');
94
98
  await (0, services_1.getAllPackageDataBatched)(prepared.uniquePackages, (batch) => {
99
+ const batchStart = lastBatchEndAt;
100
+ let batchFailedCount = 0;
95
101
  const batchItems = batch.map((batchItem) => {
96
102
  const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
97
103
  packageLookup.set(batchItem.packageName, packageInfo);
@@ -99,6 +105,8 @@ class PackageDetector {
99
105
  const isFailed = batchItem.data.latestVersion === 'unknown';
100
106
  if (isFailed) {
101
107
  failed++;
108
+ batchFailedCount++;
109
+ performanceTracker.recordFailedPackage(batchItem.packageName);
102
110
  }
103
111
  return {
104
112
  packageName: batchItem.packageName,
@@ -106,6 +114,15 @@ class PackageDetector {
106
114
  failed: isFailed,
107
115
  };
108
116
  });
117
+ const batchEnd = Date.now();
118
+ performanceTracker.recordBatch({
119
+ index: batchIndex++,
120
+ size: batch.length,
121
+ durationMs: batchEnd - batchStart,
122
+ failedCount: batchFailedCount,
123
+ });
124
+ lastBatchEndAt = batchEnd;
125
+ performanceTracker.recordCounts({ resolved, failed });
109
126
  const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
110
127
  onEvent({
111
128
  type: 'batch',
@@ -119,6 +136,7 @@ class PackageDetector {
119
136
  concurrency: this.batchConcurrency,
120
137
  });
121
138
  utils_3.debugLog.perf('PackageDetector', `registry fetch (${resolved}/${prepared.uniquePackages.length} resolved)`, tFetch);
139
+ performanceTracker.recordPhaseDuration('registryFetch', Date.now() - tFetch);
122
140
  const finalPackages = prepared.uniquePackages.flatMap((packageName) => packageLookup.get(packageName) ?? []);
123
141
  const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, false);
124
142
  utils_3.debugLog.perf('PackageDetector', `total scan complete (${finalPackages.filter((p) => p.isOutdated).length} outdated of ${finalPackages.length} deps)`, t0);
@@ -133,12 +151,15 @@ class PackageDetector {
133
151
  return finalPackages;
134
152
  }
135
153
  async prepareDependencies() {
154
+ const performanceTracker = (0, debug_1.getPerformanceTracker)();
136
155
  this.showProgress('🔍 Scanning repository for package.json files...');
137
156
  const tScan = Date.now();
138
157
  const allPackageJsonFiles = await this.findPackageJsonFilesWithTimeout(30000);
139
158
  utils_3.debugLog.perf('PackageDetector', `file scan (${allPackageJsonFiles.length} files)`, tScan, {
140
159
  files: allPackageJsonFiles,
141
160
  });
161
+ performanceTracker.recordPhaseDuration('discovery', Date.now() - tScan);
162
+ performanceTracker.recordCounts({ packageJsonFiles: allPackageJsonFiles.length });
142
163
  this.showProgress(`🔍 Found ${allPackageJsonFiles.length} package.json file${allPackageJsonFiles.length === 1 ? '' : 's'}`);
143
164
  this.showProgress('🔍 Reading dependencies from package.json files...');
144
165
  const tDeps = Date.now();
@@ -147,7 +168,10 @@ class PackageDetector {
147
168
  includeOptionalDeps: true,
148
169
  });
149
170
  utils_3.debugLog.perf('PackageDetector', `dependency collection (${allDepsRaw.length} raw deps)`, tDeps);
171
+ performanceTracker.recordPhaseDuration('depCollection', Date.now() - tDeps);
172
+ performanceTracker.recordCounts({ rawDependencies: allDepsRaw.length });
150
173
  this.showProgress('🔍 Identifying unique packages...');
174
+ const tFilter = Date.now();
151
175
  const uniquePackageNames = new Set();
152
176
  const allDependencies = [];
153
177
  let ignoredCount = 0;
@@ -191,6 +215,12 @@ class PackageDetector {
191
215
  return a.localeCompare(b);
192
216
  });
193
217
  utils_3.debugLog.info('PackageDetector', `${uniquePackages.length} unique packages to check, ${ignoredCount} ignored`);
218
+ performanceTracker.recordPhaseDuration('filter', Date.now() - tFilter);
219
+ performanceTracker.recordCounts({
220
+ uniquePackages: uniquePackages.length,
221
+ ignoredPackages: ignoredCount,
222
+ workspaceRefsSkipped: seenWorkspaceRefs.size,
223
+ });
194
224
  const currentVersions = new Map();
195
225
  for (const dep of allDependencies) {
196
226
  if (!currentVersions.has(dep.name)) {
@@ -10,6 +10,7 @@ const interactive_ui_1 = require("../interactive-ui");
10
10
  const upgrader_1 = require("./upgrader");
11
11
  const package_manager_detector_1 = require("../services/package-manager-detector");
12
12
  const utils_1 = require("../ui/utils");
13
+ const debug_1 = require("../features/debug");
13
14
  /**
14
15
  * Main orchestrator for the inup upgrade process
15
16
  */
@@ -35,6 +36,9 @@ class UpgradeRunner {
35
36
  try {
36
37
  // Check prerequisites
37
38
  this.checkPrerequisites();
39
+ const performanceTracker = (0, debug_1.getPerformanceTracker)();
40
+ performanceTracker.start();
41
+ performanceTracker.setPackageManager(this.packageManager.name);
38
42
  const progress = {
39
43
  discovered: 0,
40
44
  resolved: 0,
@@ -71,6 +75,7 @@ class UpgradeRunner {
71
75
  progress.total = event.payload.progress.total;
72
76
  progress.failed = event.payload.progress.failed;
73
77
  progress.isLoading = event.payload.progress.isLoading;
78
+ performanceTracker.mark('firstBatch');
74
79
  this.ui.appendOutdatedBatchToSelectionStates(selectionStates, event.payload.batch, previousSelections);
75
80
  refreshUI?.();
76
81
  }
@@ -81,6 +86,8 @@ class UpgradeRunner {
81
86
  progress.total = event.payload.progress.total;
82
87
  progress.failed = event.payload.progress.failed;
83
88
  progress.isLoading = event.payload.progress.isLoading;
89
+ performanceTracker.mark('firstBatch');
90
+ performanceTracker.mark('allLoaded');
84
91
  refreshUI?.();
85
92
  }
86
93
  });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types/debug.types"), exports);
18
+ __exportStar(require("./services/performance-tracker"), exports);
19
+ __exportStar(require("./renderer/performance-modal"), exports);
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderPerformanceModal = renderPerformanceModal;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const modal_1 = require("../../../ui/modal");
9
+ function formatMs(value) {
10
+ if (value === undefined || value === null)
11
+ return chalk_1.default.gray('—');
12
+ return chalk_1.default.yellow(`${value} ms`);
13
+ }
14
+ function formatCount(value) {
15
+ if (value === undefined)
16
+ return chalk_1.default.gray('—');
17
+ return chalk_1.default.cyan(String(value));
18
+ }
19
+ function labelValue(label, value, labelWidth = 22) {
20
+ const padded = label.padEnd(labelWidth, ' ');
21
+ return `${chalk_1.default.white(padded)} ${value}`;
22
+ }
23
+ function buildSections(snapshot) {
24
+ const { phases, counts, batches, failedPackages, packageManager, totalMs } = snapshot;
25
+ const pinned = [
26
+ {
27
+ key: 'header',
28
+ rows: [
29
+ chalk_1.default.cyan('⚡ Performance'),
30
+ chalk_1.default.gray(`Package manager: ${packageManager ? chalk_1.default.white(packageManager) : chalk_1.default.gray('unknown')}`),
31
+ ],
32
+ required: true,
33
+ behavior: 'pinned',
34
+ },
35
+ ];
36
+ const bodyRows = [];
37
+ bodyRows.push(chalk_1.default.bold('Timings'));
38
+ bodyRows.push(labelValue('Discovery', formatMs(phases.discovery)));
39
+ bodyRows.push(labelValue('Dep collection', formatMs(phases.depCollection)));
40
+ bodyRows.push(labelValue('Filter', formatMs(phases.filter)));
41
+ bodyRows.push(labelValue('Registry fetch', formatMs(phases.registryFetch)));
42
+ bodyRows.push(labelValue('First batch ready', formatMs(phases.firstBatch)));
43
+ bodyRows.push(labelValue('All packages loaded', formatMs(phases.allLoaded)));
44
+ bodyRows.push(labelValue('Elapsed total', formatMs(totalMs ?? undefined)));
45
+ bodyRows.push('');
46
+ bodyRows.push(chalk_1.default.bold('Counts'));
47
+ bodyRows.push(labelValue('package.json files', formatCount(counts.packageJsonFiles)));
48
+ bodyRows.push(labelValue('Raw dependencies', formatCount(counts.rawDependencies)));
49
+ bodyRows.push(labelValue('Unique packages', formatCount(counts.uniquePackages)));
50
+ bodyRows.push(labelValue('Ignored', formatCount(counts.ignoredPackages)));
51
+ bodyRows.push(labelValue('Workspace refs', formatCount(counts.workspaceRefsSkipped)));
52
+ bodyRows.push(labelValue('Resolved', formatCount(counts.resolved)));
53
+ bodyRows.push(labelValue('Failed', formatCount(counts.failed)));
54
+ bodyRows.push('');
55
+ bodyRows.push(chalk_1.default.bold('Batches'));
56
+ if (batches.length > 0) {
57
+ const durations = batches.map((b) => b.durationMs);
58
+ const total = durations.reduce((a, b) => a + b, 0);
59
+ const avg = Math.round(total / batches.length);
60
+ const slowest = Math.max(...durations);
61
+ const slowestBatch = batches.find((b) => b.durationMs === slowest);
62
+ bodyRows.push(labelValue('Batch count', formatCount(batches.length)));
63
+ bodyRows.push(labelValue('Avg batch', formatMs(avg)));
64
+ bodyRows.push(labelValue('Slowest batch', `${formatMs(slowest)} ${chalk_1.default.gray(`(#${slowestBatch?.index})`)}`));
65
+ }
66
+ else {
67
+ bodyRows.push(chalk_1.default.gray(' (no batches recorded)'));
68
+ }
69
+ bodyRows.push('');
70
+ bodyRows.push(chalk_1.default.bold('Failures'));
71
+ if (failedPackages.length > 0) {
72
+ for (const name of failedPackages) {
73
+ bodyRows.push(` ${chalk_1.default.red('✗')} ${name}`);
74
+ }
75
+ }
76
+ else {
77
+ bodyRows.push(chalk_1.default.gray(' (none)'));
78
+ }
79
+ const body = [
80
+ {
81
+ key: 'body',
82
+ rows: bodyRows,
83
+ required: true,
84
+ behavior: 'body',
85
+ },
86
+ ];
87
+ return { pinned, body };
88
+ }
89
+ function renderPerformanceModal(snapshot, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
90
+ const modalWidth = (0, modal_1.getModalWidth)(terminalWidth, 60, 84);
91
+ const padding = Math.floor((terminalWidth - modalWidth) / 2);
92
+ const fixedModalHeight = Math.max(10, terminalHeight - 2);
93
+ const { pinned, body } = buildSections(snapshot);
94
+ const pinnedRowCount = pinned.reduce((sum, s) => sum + s.rows.length, 0);
95
+ // frame: top border + pinned rows + separator + body rows + bottom border
96
+ const availableForBody = Math.max(3, fixedModalHeight - 2 - pinnedRowCount - 1);
97
+ const bodyRows = body[0].rows;
98
+ const totalScrollableRows = bodyRows.length;
99
+ const needsScroll = totalScrollableRows > availableForBody;
100
+ const visibleBodyRows = needsScroll ? Math.max(1, availableForBody - 1) : availableForBody;
101
+ const maxScroll = Math.max(0, totalScrollableRows - visibleBodyRows);
102
+ const clampedOffset = Math.min(Math.max(0, scrollOffset), maxScroll);
103
+ const visibleSlice = bodyRows.slice(clampedOffset, clampedOffset + visibleBodyRows);
104
+ const lines = [];
105
+ const topPadding = Math.max(0, Math.floor((terminalHeight - fixedModalHeight) / 2));
106
+ for (let i = 0; i < topPadding; i++) {
107
+ lines.push('');
108
+ }
109
+ lines.push(' '.repeat(padding) + chalk_1.default.gray('╭' + '─'.repeat(modalWidth - 2) + '╮'));
110
+ for (const section of pinned) {
111
+ for (const row of section.rows) {
112
+ lines.push((0, modal_1.renderModalRow)(padding, modalWidth, row));
113
+ }
114
+ }
115
+ lines.push((0, modal_1.renderModalSeparator)(padding, modalWidth));
116
+ let renderedBodyRows = 0;
117
+ for (const row of visibleSlice) {
118
+ lines.push((0, modal_1.renderModalRow)(padding, modalWidth, row));
119
+ renderedBodyRows++;
120
+ }
121
+ const footer = needsScroll
122
+ ? chalk_1.default.gray(`Lines ${clampedOffset + 1}-${Math.min(clampedOffset + visibleBodyRows, totalScrollableRows)} of ${totalScrollableRows}`)
123
+ : null;
124
+ const usedContentRows = pinnedRowCount + 1 + renderedBodyRows + (footer ? 1 : 0);
125
+ const totalContentSlots = fixedModalHeight - 2;
126
+ const emptyRows = Math.max(0, totalContentSlots - usedContentRows);
127
+ for (let i = 0; i < emptyRows; i++) {
128
+ lines.push((0, modal_1.renderModalRow)(padding, modalWidth, ''));
129
+ }
130
+ if (footer) {
131
+ lines.push((0, modal_1.renderModalRow)(padding, modalWidth, footer));
132
+ }
133
+ lines.push(' '.repeat(padding) + chalk_1.default.gray('╰' + '─'.repeat(modalWidth - 2) + '╯'));
134
+ return {
135
+ lines,
136
+ maxScrollOffset: maxScroll,
137
+ totalContentRows: totalScrollableRows,
138
+ usesInternalScroll: needsScroll,
139
+ };
140
+ }
141
+ //# sourceMappingURL=performance-modal.js.map
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPerformanceTracker = getPerformanceTracker;
4
+ class PerformanceTracker {
5
+ constructor() {
6
+ this.startedAt = null;
7
+ this.phases = {};
8
+ this.counts = {};
9
+ this.batches = [];
10
+ this.failedPackages = [];
11
+ this.packageManager = null;
12
+ }
13
+ start() {
14
+ this.startedAt = Date.now();
15
+ this.phases = {};
16
+ this.counts = {};
17
+ this.batches = [];
18
+ this.failedPackages = [];
19
+ this.packageManager = null;
20
+ }
21
+ mark(phase) {
22
+ if (this.startedAt === null || this.phases[phase] !== undefined)
23
+ return;
24
+ this.phases[phase] = Date.now() - this.startedAt;
25
+ }
26
+ recordPhaseDuration(phase, durationMs) {
27
+ this.phases[phase] = durationMs;
28
+ }
29
+ recordCounts(partial) {
30
+ this.counts = { ...this.counts, ...partial };
31
+ }
32
+ recordBatch(batch) {
33
+ this.batches.push(batch);
34
+ }
35
+ recordFailedPackage(name) {
36
+ if (!this.failedPackages.includes(name)) {
37
+ this.failedPackages.push(name);
38
+ }
39
+ }
40
+ setPackageManager(name) {
41
+ this.packageManager = name;
42
+ }
43
+ snapshot() {
44
+ const totalMs = this.startedAt === null
45
+ ? null
46
+ : (this.phases.allLoaded ?? Date.now() - this.startedAt);
47
+ return {
48
+ startedAt: this.startedAt,
49
+ phases: { ...this.phases },
50
+ totalMs,
51
+ counts: { ...this.counts },
52
+ batches: [...this.batches],
53
+ failedPackages: [...this.failedPackages],
54
+ packageManager: this.packageManager,
55
+ };
56
+ }
57
+ reset() {
58
+ this.startedAt = null;
59
+ this.phases = {};
60
+ this.counts = {};
61
+ this.batches = [];
62
+ this.failedPackages = [];
63
+ this.packageManager = null;
64
+ }
65
+ }
66
+ let instance = null;
67
+ function getPerformanceTracker() {
68
+ if (!instance)
69
+ instance = new PerformanceTracker();
70
+ return instance;
71
+ }
72
+ //# sourceMappingURL=performance-tracker.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=debug.types.js.map
@@ -43,6 +43,7 @@ const ui_1 = require("./ui");
43
43
  const controllers_1 = require("./ui/controllers");
44
44
  const themes_1 = require("./ui/themes");
45
45
  const themes_colors_1 = require("./ui/themes-colors");
46
+ const debug_1 = require("./features/debug");
46
47
  const DEFAULT_VULNERABILITY_DISPLAY_OPTIONS = {
47
48
  showPeerDependencyVulnerabilities: false,
48
49
  showOptionalDependencyVulnerabilities: false,
@@ -236,6 +237,8 @@ class InteractiveUI {
236
237
  stateManager.setRenderableItems([]);
237
238
  // Track the current max scroll offset for the info modal
238
239
  let infoModalMaxScrollOffset = 0;
240
+ // Track the current max scroll offset for the debug modal
241
+ let debugModalMaxScrollOffset = 0;
239
242
  let previousViewportMode = null;
240
243
  let previousModalViewportLineCount = null;
241
244
  const handleAction = (action) => {
@@ -332,6 +335,19 @@ class InteractiveUI {
332
335
  return;
333
336
  }
334
337
  break;
338
+ case 'toggle_debug_modal':
339
+ stateManager.toggleDebugModal();
340
+ break;
341
+ case 'scroll_debug_modal_up':
342
+ if (!stateManager.scrollDebugModalUp()) {
343
+ return;
344
+ }
345
+ break;
346
+ case 'scroll_debug_modal_down':
347
+ if (!stateManager.scrollDebugModalDown(debugModalMaxScrollOffset)) {
348
+ return;
349
+ }
350
+ break;
335
351
  case 'navigate_info_modal_version':
336
352
  {
337
353
  if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
@@ -511,6 +527,18 @@ class InteractiveUI {
511
527
  const modalLines = this.renderer.renderThemeSelectorModal(themeManager.getCurrentTheme(), themeManager.getPreviewTheme(), terminalWidth, terminalHeight);
512
528
  renderModalViewport('theme-modal', chalk_1.default.bold.white('T ') + chalk_1.default.gray('/ Esc Exit theme selector'), modalLines, terminalWidth, terminalHeight, bgCode);
513
529
  }
530
+ else if (uiState.showDebugModal) {
531
+ const terminalWidth = process.stdout.columns || 80;
532
+ const terminalHeight = this.getTerminalHeight();
533
+ const snapshot = (0, debug_1.getPerformanceTracker)().snapshot();
534
+ const result = (0, debug_1.renderPerformanceModal)(snapshot, terminalWidth, Math.max(8, terminalHeight - 4), uiState.debugModalScrollOffset);
535
+ debugModalMaxScrollOffset = result.maxScrollOffset;
536
+ stateManager.clampDebugModalScrollOffset(debugModalMaxScrollOffset);
537
+ const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
538
+ ? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
539
+ : '';
540
+ renderModalViewport('info-modal', scrollHint + chalk_1.default.bold.white('! / Esc ') + chalk_1.default.gray('Close'), result.lines, terminalWidth, terminalHeight, bgCode);
541
+ }
514
542
  else if (uiState.showInfoModal &&
515
543
  uiState.infoModalRow >= 0 &&
516
544
  uiState.infoModalRow < filteredStates.length) {
@@ -36,11 +36,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getAllPackageData = getAllPackageData;
37
37
  exports.getAllPackageDataBatched = getAllPackageDataBatched;
38
38
  exports.clearPackageCache = clearPackageCache;
39
+ exports.closeRegistryPool = closeRegistryPool;
39
40
  const semver = __importStar(require("semver"));
41
+ const undici_1 = require("undici");
42
+ const node_zlib_1 = require("node:zlib");
43
+ const node_util_1 = require("node:util");
44
+ const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
45
+ const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
46
+ const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
40
47
  const config_1 = require("../config");
41
48
  const jsdelivr_registry_1 = require("./jsdelivr-registry");
42
49
  const utils_1 = require("../ui/utils");
43
50
  const inFlightLookups = new Map();
51
+ const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
52
+ const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
53
+ const registryPool = new undici_1.Pool(registryOrigin, {
54
+ connections: 64,
55
+ pipelining: 10,
56
+ keepAliveTimeout: 30000,
57
+ keepAliveMaxTimeout: 600000,
58
+ headersTimeout: config_1.REQUEST_TIMEOUT,
59
+ bodyTimeout: config_1.REQUEST_TIMEOUT,
60
+ allowH2: false,
61
+ });
44
62
  const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
45
63
  const isTransientNetworkError = (error) => {
46
64
  if (!(error instanceof Error)) {
@@ -48,6 +66,14 @@ const isTransientNetworkError = (error) => {
48
66
  }
49
67
  const maybeCode = error.code;
50
68
  return (error.name === 'AbortError' ||
69
+ error.name === 'HeadersTimeoutError' ||
70
+ error.name === 'BodyTimeoutError' ||
71
+ error.name === 'ConnectTimeoutError' ||
72
+ error.name === 'SocketError' ||
73
+ maybeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
74
+ maybeCode === 'UND_ERR_BODY_TIMEOUT' ||
75
+ maybeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
76
+ maybeCode === 'UND_ERR_SOCKET' ||
51
77
  maybeCode === 'ENOTFOUND' ||
52
78
  maybeCode === 'EAI_AGAIN' ||
53
79
  maybeCode === 'ECONNRESET' ||
@@ -71,43 +97,56 @@ async function getFreshPackageData(packageName, currentVersion) {
71
97
  inFlightLookups.set(cacheKey, lookupPromise);
72
98
  return await lookupPromise;
73
99
  }
74
- /**
75
- * Fetches package data from npm registry.
76
- * Falls back to jsDelivr when npm is temporarily unavailable.
77
- */
100
+ function parseVersions(raw) {
101
+ const data = JSON.parse(raw);
102
+ const allVersions = Object.keys(data.versions || {}).filter((v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v));
103
+ const sortedVersions = allVersions.sort(semver.rcompare);
104
+ const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
105
+ return { latestVersion, allVersions };
106
+ }
78
107
  async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
79
108
  try {
80
- const url = `${config_1.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`;
81
- const controller = new AbortController();
82
- const timeoutId = setTimeout(() => controller.abort(), config_1.REQUEST_TIMEOUT);
83
- try {
84
- const response = await fetch(url, {
85
- method: 'GET',
86
- headers: {
87
- accept: 'application/vnd.npm.install-v1+json',
88
- },
89
- signal: controller.signal,
90
- });
91
- clearTimeout(timeoutId);
92
- if (!response.ok) {
93
- if (isRetryableStatus(response.status)) {
94
- return await fetchFromJsdelivrFallback(packageName, currentVersion);
95
- }
96
- throw new Error(`HTTP ${response.status}`);
109
+ const encodedName = packageName.startsWith('@')
110
+ ? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
111
+ : encodeURIComponent(packageName);
112
+ const path = `${registryPathPrefix}/${encodedName}`;
113
+ const { statusCode, headers, body } = await registryPool.request({
114
+ path,
115
+ method: 'GET',
116
+ headers: {
117
+ accept: 'application/vnd.npm.install-v1+json',
118
+ 'accept-encoding': 'gzip, deflate, br',
119
+ },
120
+ headersTimeout: config_1.REQUEST_TIMEOUT,
121
+ bodyTimeout: config_1.REQUEST_TIMEOUT,
122
+ blocking: false,
123
+ });
124
+ if (statusCode < 200 || statusCode >= 300) {
125
+ await body.dump();
126
+ if (isRetryableStatus(statusCode)) {
127
+ return await fetchFromJsdelivrFallback(packageName, currentVersion);
97
128
  }
98
- const text = await response.text();
99
- const data = JSON.parse(text);
100
- const allVersions = Object.keys(data.versions || {}).filter((version) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version));
101
- const sortedVersions = allVersions.sort(semver.rcompare);
102
- const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
103
- return {
104
- latestVersion,
105
- allVersions,
106
- };
129
+ return { latestVersion: 'unknown', allVersions: [] };
107
130
  }
108
- finally {
109
- clearTimeout(timeoutId);
131
+ const raw = Buffer.from(await body.arrayBuffer());
132
+ const encodingHeader = headers['content-encoding'];
133
+ const encoding = (Array.isArray(encodingHeader) ? encodingHeader[0] : encodingHeader)
134
+ ?.toString()
135
+ .toLowerCase();
136
+ let decoded;
137
+ if (encoding === 'gzip') {
138
+ decoded = await gunzipAsync(raw);
110
139
  }
140
+ else if (encoding === 'br') {
141
+ decoded = await brotliDecompressAsync(raw);
142
+ }
143
+ else if (encoding === 'deflate') {
144
+ decoded = await inflateAsync(raw);
145
+ }
146
+ else {
147
+ decoded = raw;
148
+ }
149
+ return parseVersions(decoded.toString('utf8'));
111
150
  }
112
151
  catch (error) {
113
152
  if (isTransientNetworkError(error)) {
@@ -118,7 +157,7 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
118
157
  }
119
158
  /**
120
159
  * Fetches package version data from npm registry for multiple packages.
121
- * Uses native fetch with timeout support for reliable performance.
160
+ * Uses an undici Pool with HTTP/1.1 pipelining for high throughput.
122
161
  * Only returns valid semantic versions (X.Y.Z format, excluding pre-releases).
123
162
  */
124
163
  async function getAllPackageData(packageNames, onProgress, currentVersions) {
@@ -136,9 +175,7 @@ async function getAllPackageData(packageNames, onProgress, currentVersions) {
136
175
  onProgress(packageName, completedCount, total);
137
176
  }
138
177
  });
139
- // Wait for all requests to complete
140
178
  await Promise.all(allPromises);
141
- // Clear the progress line if no custom progress handler
142
179
  if (!onProgress) {
143
180
  utils_1.ConsoleUtils.clearProgress();
144
181
  }
@@ -171,11 +208,23 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
171
208
  let completedCount = 0;
172
209
  let batchStart = 0;
173
210
  let batchIndex = 0;
211
+ const batchPromises = [];
212
+ const pendingEmissions = new Map();
213
+ let nextEmitIndex = 0;
214
+ const flushPending = () => {
215
+ while (pendingEmissions.has(nextEmitIndex)) {
216
+ const ready = pendingEmissions.get(nextEmitIndex);
217
+ pendingEmissions.delete(nextEmitIndex);
218
+ onBatchReady?.(ready);
219
+ nextEmitIndex++;
220
+ }
221
+ };
174
222
  while (batchStart < packageNames.length) {
175
223
  const batchSize = batchSizes[Math.min(batchIndex, batchSizes.length - 1)];
176
224
  const batchNames = packageNames.slice(batchStart, batchStart + batchSize);
225
+ const capturedBatchIndex = batchIndex;
177
226
  const batchResults = new Array(batchNames.length);
178
- await runWithConcurrencyLimit(batchNames, concurrency, async (packageName, itemIndex) => {
227
+ const batchPromise = runWithConcurrencyLimit(batchNames, concurrency, async (packageName, itemIndex) => {
179
228
  const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
180
229
  packageData.set(packageName, data);
181
230
  completedCount++;
@@ -184,14 +233,18 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
184
233
  data,
185
234
  completed: completedCount,
186
235
  total,
187
- batchIndex,
236
+ batchIndex: capturedBatchIndex,
188
237
  itemIndex,
189
238
  };
239
+ }).then(() => {
240
+ pendingEmissions.set(capturedBatchIndex, batchResults.filter(Boolean));
241
+ flushPending();
190
242
  });
191
- onBatchReady?.(batchResults.filter(Boolean));
243
+ batchPromises.push(batchPromise);
192
244
  batchStart += batchSize;
193
245
  batchIndex++;
194
246
  }
247
+ await Promise.all(batchPromises);
195
248
  return packageData;
196
249
  }
197
250
  /**
@@ -200,4 +253,7 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
200
253
  function clearPackageCache() {
201
254
  inFlightLookups.clear();
202
255
  }
256
+ async function closeRegistryPool() {
257
+ await registryPool.close();
258
+ }
203
259
  //# sourceMappingURL=npm-registry.js.map
@@ -47,6 +47,29 @@ class InputHandler {
47
47
  }
48
48
  return;
49
49
  }
50
+ // Handle debug modal input (scroll and close)
51
+ if (uiState.showDebugModal) {
52
+ if (str === '!') {
53
+ this.onAction({ type: 'toggle_debug_modal' });
54
+ return;
55
+ }
56
+ if (key) {
57
+ switch (key.name) {
58
+ case 'escape':
59
+ this.onAction({ type: 'toggle_debug_modal' });
60
+ return;
61
+ case 'up':
62
+ this.onAction({ type: 'scroll_debug_modal_up' });
63
+ return;
64
+ case 'down':
65
+ this.onAction({ type: 'scroll_debug_modal_down' });
66
+ return;
67
+ default:
68
+ return; // consume other keys while modal is open
69
+ }
70
+ }
71
+ return;
72
+ }
50
73
  // Handle info modal input (scroll and close)
51
74
  if (uiState.showInfoModal) {
52
75
  if (key) {
@@ -76,6 +99,11 @@ class InputHandler {
76
99
  }
77
100
  return;
78
101
  }
102
+ // '!' toggles the debug/performance modal (outside of filter mode)
103
+ if (str === '!' && !uiState.filterMode) {
104
+ this.onAction({ type: 'toggle_debug_modal' });
105
+ return;
106
+ }
79
107
  // Check for '/' character to handle filter mode (only when not in modal)
80
108
  if (str === '/') {
81
109
  if (uiState.filterMode) {
@@ -9,8 +9,42 @@ class ModalManager {
9
9
  isLoadingModalInfo: false,
10
10
  infoModalScrollOffset: 0,
11
11
  infoModalSessionId: 0,
12
+ showDebugModal: false,
13
+ debugModalScrollOffset: 0,
12
14
  };
13
15
  }
16
+ isDebugModalOpen() {
17
+ return this.state.showDebugModal;
18
+ }
19
+ toggleDebugModal() {
20
+ this.state.showDebugModal = !this.state.showDebugModal;
21
+ this.state.debugModalScrollOffset = 0;
22
+ }
23
+ closeDebugModal() {
24
+ this.state.showDebugModal = false;
25
+ this.state.debugModalScrollOffset = 0;
26
+ }
27
+ scrollDebugModalUp() {
28
+ if (this.state.debugModalScrollOffset > 0) {
29
+ this.state.debugModalScrollOffset--;
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ scrollDebugModalDown(maxOffset) {
35
+ if (this.state.debugModalScrollOffset < maxOffset) {
36
+ this.state.debugModalScrollOffset++;
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ clampDebugModalScrollOffset(maxOffset) {
42
+ const nextOffset = Math.max(0, Math.min(this.state.debugModalScrollOffset, maxOffset));
43
+ if (nextOffset === this.state.debugModalScrollOffset)
44
+ return false;
45
+ this.state.debugModalScrollOffset = nextOffset;
46
+ return true;
47
+ }
14
48
  getState() {
15
49
  return { ...this.state };
16
50
  }
@@ -43,6 +43,8 @@ class StateManager {
43
43
  infoModalRow: modalState.infoModalRow,
44
44
  isLoadingModalInfo: modalState.isLoadingModalInfo,
45
45
  infoModalScrollOffset: modalState.infoModalScrollOffset,
46
+ showDebugModal: modalState.showDebugModal,
47
+ debugModalScrollOffset: modalState.debugModalScrollOffset,
46
48
  filterMode: filterState.filterMode,
47
49
  filterQuery: filterState.filterQuery,
48
50
  showThemeModal: themeState.showThemeModal,
@@ -187,6 +189,21 @@ class StateManager {
187
189
  clampInfoModalScrollOffset(maxOffset) {
188
190
  return this.modalManager.clampScrollOffset(maxOffset);
189
191
  }
192
+ toggleDebugModal() {
193
+ this.modalManager.toggleDebugModal();
194
+ }
195
+ closeDebugModal() {
196
+ this.modalManager.closeDebugModal();
197
+ }
198
+ scrollDebugModalUp() {
199
+ return this.modalManager.scrollDebugModalUp();
200
+ }
201
+ scrollDebugModalDown(maxOffset) {
202
+ return this.modalManager.scrollDebugModalDown(maxOffset);
203
+ }
204
+ clampDebugModalScrollOffset(maxOffset) {
205
+ return this.modalManager.clampDebugModalScrollOffset(maxOffset);
206
+ }
190
207
  // Filter delegation
191
208
  enterFilterMode(preserveQuery = false) {
192
209
  this.filterManager.enterFilterMode(preserveQuery);
@@ -60,7 +60,7 @@ exports.TerminalInput = {
60
60
  },
61
61
  };
62
62
  },
63
- promptForConfirmation(prompt) {
63
+ promptForConfirmation(prompt, defaultValue = true) {
64
64
  return new Promise((resolve) => {
65
65
  const rl = readline.createInterface({
66
66
  input: process.stdin,
@@ -73,10 +73,53 @@ exports.TerminalInput = {
73
73
  };
74
74
  rl.question(prompt, (answer) => {
75
75
  const normalizedAnswer = answer.trim().toLowerCase();
76
- finish(normalizedAnswer === '' || normalizedAnswer === 'y' || normalizedAnswer === 'yes');
76
+ if (normalizedAnswer === '') {
77
+ finish(defaultValue);
78
+ return;
79
+ }
80
+ finish(normalizedAnswer === 'y' || normalizedAnswer === 'yes');
77
81
  });
78
82
  rl.on('SIGINT', () => finish(false));
79
83
  });
80
84
  },
85
+ promptForImmediateConfirmation(prompt, defaultValue = true) {
86
+ return new Promise((resolve) => {
87
+ process.stdout.write(prompt);
88
+ let cleanup = () => { };
89
+ const finish = (value) => {
90
+ cleanup();
91
+ process.stdout.write('\n');
92
+ resolve(value);
93
+ };
94
+ try {
95
+ const session = exports.TerminalInput.startKeypressSession((str, key) => {
96
+ const normalized = str.trim().toLowerCase();
97
+ if (key.name === 'return' || key.name === 'enter') {
98
+ finish(defaultValue);
99
+ return;
100
+ }
101
+ if (normalized === 'y') {
102
+ finish(true);
103
+ return;
104
+ }
105
+ if (normalized === 'n') {
106
+ finish(false);
107
+ return;
108
+ }
109
+ if (key.ctrl && key.name === 'c') {
110
+ finish(false);
111
+ }
112
+ });
113
+ cleanup = () => {
114
+ session.close();
115
+ };
116
+ }
117
+ catch {
118
+ exports.TerminalInput.promptForConfirmation(prompt, defaultValue)
119
+ .then(resolve)
120
+ .catch(() => resolve(false));
121
+ }
122
+ });
123
+ },
81
124
  };
82
125
  //# sourceMappingURL=terminal-input.js.map
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getGitWorkingTreeState = getGitWorkingTreeState;
4
+ const child_process_1 = require("child_process");
5
+ /**
6
+ * Detect whether cwd is a git work tree and whether it has local changes.
7
+ * Fail soft if git is unavailable or cwd is not a repository.
8
+ */
9
+ function getGitWorkingTreeState(cwd) {
10
+ try {
11
+ const isInsideWorkTree = (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', {
12
+ cwd,
13
+ encoding: 'utf-8',
14
+ stdio: ['ignore', 'pipe', 'ignore'],
15
+ }).trim();
16
+ if (isInsideWorkTree !== 'true') {
17
+ return { isRepo: false, isDirty: false };
18
+ }
19
+ const status = (0, child_process_1.execSync)('git status --porcelain', {
20
+ cwd,
21
+ encoding: 'utf-8',
22
+ stdio: ['ignore', 'pipe', 'ignore'],
23
+ });
24
+ return {
25
+ isRepo: true,
26
+ isDirty: status.trim().length > 0,
27
+ };
28
+ }
29
+ catch {
30
+ return { isRepo: false, isDirty: false };
31
+ }
32
+ }
33
+ //# sourceMappingURL=git.js.map
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.collectAllDependenciesAsync = exports.readPackageJsonAsync = void 0;
21
21
  __exportStar(require("./filesystem"), exports);
22
22
  __exportStar(require("./exec"), exports);
23
+ __exportStar(require("./git"), exports);
23
24
  __exportStar(require("./version"), exports);
24
25
  __exportStar(require("./debug-logger"), exports);
25
26
  // Re-export async functions for convenience
package/package.json CHANGED
@@ -1,26 +1,33 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.5.0",
4
- "description": "Interactive CLI tool for upgrading dependencies with ease. Auto-detects and works with npm, yarn, pnpm, and bun. Inspired by yarn upgrade-interactive. Supports monorepos, workspaces, and batch upgrades.",
3
+ "version": "1.5.2",
4
+ "description": "Interactive dependency upgrader for npm, yarn, pnpm & bun. Zero-config, monorepo-ready. Upgrade-interactive for every package manager.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "inup": "./dist/cli.js"
8
8
  },
9
9
  "author": "Donfear",
10
10
  "keywords": [
11
+ "upgrade-interactive",
12
+ "interactive",
13
+ "dependency-management",
14
+ "outdated",
15
+ "upgrade",
16
+ "update",
11
17
  "npm",
12
18
  "yarn",
13
19
  "pnpm",
14
20
  "bun",
15
- "upgrade",
16
- "interactive",
17
- "cli",
18
- "package-manager",
19
- "dependency-management",
20
- "yarn-upgrade-interactive",
21
21
  "monorepo",
22
22
  "workspace",
23
- "batch-upgrade"
23
+ "cli",
24
+ "vulnerability",
25
+ "audit",
26
+ "changelog",
27
+ "package-manager",
28
+ "dependencies",
29
+ "semver",
30
+ "ncu"
24
31
  ],
25
32
  "license": "MIT",
26
33
  "homepage": "https://github.com/donfear/inup#readme",