inup 1.4.2 → 1.4.4

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 CHANGED
@@ -1,14 +1,14 @@
1
- # inup
1
+ # 🚀 inup
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
6
 
7
- Interactive upgrade for your dependencies. Works with npm, yarn, pnpm, and bun.
7
+ Upgrade your dependencies interactively. Works with npm, yarn, pnpm, and bun.
8
8
 
9
9
  ![Interactive Upgrade Demo](docs/demo/interactive-upgrade.gif)
10
10
 
11
- ## Install
11
+ ## 🚀 Usage
12
12
 
13
13
  ```bash
14
14
  npx inup
@@ -20,24 +20,17 @@ Or install globally:
20
20
  npm install -g inup
21
21
  ```
22
22
 
23
- ## Usage
24
-
25
- ```bash
26
- npx inup
27
- ```
28
-
29
23
  That's it. The tool scans your project, finds outdated packages, and lets you pick what to upgrade.
30
24
 
31
- ## Features
25
+ ## 💡 Why inup?
32
26
 
33
- - Auto-detects package manager (npm, yarn, pnpm, bun)
34
- - Works with monorepos and workspaces
35
- - Batch upgrades with keyboard shortcuts
36
- - Search packages with `/`
37
- - Multiple themes (press `t`)
38
- - Package info modal (press `i`)
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`.
39
32
 
40
- ## Keyboard Shortcuts
33
+ ## ⌨️ Keyboard Shortcuts
41
34
 
42
35
  - `↑/↓` - Navigate packages
43
36
  - `←/→` - Select version (current, patch, minor, major)
@@ -50,18 +43,22 @@ That's it. The tool scans your project, finds outdated packages, and lets you pi
50
43
  - `i` - View package info
51
44
  - `Enter` - Confirm and upgrade
52
45
 
53
- ## Options
46
+ ## ⚙️ Options
54
47
 
55
48
  ```bash
56
49
  inup [options]
57
50
 
58
51
  -d, --dir <path> Run in specific directory
59
52
  -e, --exclude <patterns> Skip directories (comma-separated regex)
60
- -p, --peer Include peer dependencies
61
- -o, --optional Include optional dependencies
62
53
  --package-manager <name> Force package manager (npm, yarn, pnpm, bun)
63
54
  ```
64
55
 
65
- ## License
56
+ ## 🔒 Privacy
57
+
58
+ We don't track anything. Ever.
59
+
60
+ The only network requests made are to the npm registry and jsDelivr CDN to fetch package version data. That's it.
61
+
62
+ ## 📄 License
66
63
 
67
64
  MIT
package/dist/cli.js CHANGED
@@ -18,8 +18,6 @@ program
18
18
  .version(packageJson.version)
19
19
  .option('-d, --dir <directory>', 'specify directory to run in', process.cwd())
20
20
  .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
21
- .option('-p, --peer', 'include peer dependencies in upgrade process')
22
- .option('-o, --optional', 'include optional dependencies in upgrade process')
23
21
  .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
24
22
  .action(async (options) => {
25
23
  console.log(chalk_1.default.bold.blue(`🚀 `) + chalk_1.default.bold.red(`i`) + chalk_1.default.bold.yellow(`n`) + chalk_1.default.bold.blue(`u`) + chalk_1.default.bold.magenta(`p`) + `\n`);
@@ -31,10 +29,6 @@ program
31
29
  .map((p) => p.trim())
32
30
  .filter(Boolean)
33
31
  : [];
34
- // Commander.js: boolean flags are undefined if not provided, true if provided
35
- // Both flags default to false (opt-in)
36
- const includePeerDeps = options.peer === true;
37
- const includeOptionalDeps = options.optional === true;
38
32
  // Validate package manager if provided
39
33
  let packageManager;
40
34
  if (options.packageManager) {
@@ -49,8 +43,6 @@ program
49
43
  const upgrader = new index_1.UpgradeRunner({
50
44
  cwd: options.dir,
51
45
  excludePatterns,
52
- includePeerDeps,
53
- includeOptionalDeps,
54
46
  packageManager,
55
47
  });
56
48
  await upgrader.run();
@@ -1,33 +1,11 @@
1
1
  "use strict";
2
- /**
3
- * Constants for npm registry queries and configuration
4
- */
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKSPACE_FILES = exports.LOCK_FILES = exports.DEFAULT_REGISTRY = exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.JSDELIVR_CDN_URL = exports.NPM_REGISTRY_URL = void 0;
3
+ exports.DEFAULT_REGISTRY = exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.JSDELIVR_CDN_URL = exports.NPM_REGISTRY_URL = exports.PACKAGE_NAME = void 0;
4
+ exports.PACKAGE_NAME = 'inup';
7
5
  exports.NPM_REGISTRY_URL = 'https://registry.npmjs.org';
8
6
  exports.JSDELIVR_CDN_URL = 'https://cdn.jsdelivr.net/npm';
9
7
  exports.MAX_CONCURRENT_REQUESTS = 150;
10
8
  exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
11
9
  exports.REQUEST_TIMEOUT = 60000; // 60 seconds in milliseconds
12
- /**
13
- * Registry selection: 'jsdelivr' for fast CDN lookups, 'npm' for direct npm registry
14
- * Set to 'npm' to use npm registry by default instead of jsdelivr
15
- */
16
10
  exports.DEFAULT_REGISTRY = 'jsdelivr';
17
- /**
18
- * Package manager lock files
19
- */
20
- exports.LOCK_FILES = {
21
- npm: 'package-lock.json',
22
- yarn: 'yarn.lock',
23
- pnpm: 'pnpm-lock.yaml',
24
- bun: 'bun.lockb',
25
- };
26
- /**
27
- * Package manager workspace files
28
- */
29
- exports.WORKSPACE_FILES = {
30
- pnpm: 'pnpm-workspace.yaml',
31
- // npm, yarn, and bun use package.json workspaces field
32
- };
33
11
  //# sourceMappingURL=constants.js.map
@@ -44,9 +44,6 @@ class PackageDetector {
44
44
  this.packageJson = null;
45
45
  this.cwd = options?.cwd || process.cwd();
46
46
  this.excludePatterns = options?.excludePatterns || [];
47
- // Explicitly check for true to ensure false/undefined both become false (opt-in)
48
- this.includePeerDeps = options?.includePeerDeps === true;
49
- this.includeOptionalDeps = options?.includeOptionalDeps === true;
50
47
  this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
51
48
  if (this.packageJsonPath) {
52
49
  this.packageJson = (0, utils_1.readPackageJson)(this.packageJsonPath);
@@ -67,8 +64,8 @@ class PackageDetector {
67
64
  // Step 2: Collect all dependencies from package.json files (parallelized)
68
65
  this.showProgress('🔍 Reading dependencies from package.json files...');
69
66
  const allDepsRaw = await (0, utils_1.collectAllDependenciesAsync)(allPackageJsonFiles, {
70
- includePeerDeps: this.includePeerDeps,
71
- includeOptionalDeps: this.includeOptionalDeps,
67
+ includePeerDeps: true,
68
+ includeOptionalDeps: true,
72
69
  });
73
70
  // Step 3: Get unique package names while filtering out workspace references
74
71
  this.showProgress('🔍 Identifying unique packages...');
@@ -91,19 +88,11 @@ class PackageDetector {
91
88
  }
92
89
  }
93
90
  const allPackageData = config_1.DEFAULT_REGISTRY === 'jsdelivr'
94
- ? await (0, services_1.getAllPackageDataFromJsdelivr)(packageNames, currentVersions, (currentPackage, completed, total) => {
95
- const percentage = Math.round((completed / total) * 100);
96
- const truncatedPackage = currentPackage.length > 40
97
- ? currentPackage.substring(0, 37) + '...'
98
- : currentPackage;
99
- this.showProgress(`🌐 Fetching ${percentage}% (${truncatedPackage})`);
91
+ ? await (0, services_1.getAllPackageDataFromJsdelivr)(packageNames, currentVersions, (_currentPackage, completed, total) => {
92
+ this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`);
100
93
  })
101
- : await (0, services_1.getAllPackageData)(packageNames, (currentPackage, completed, total) => {
102
- const percentage = Math.round((completed / total) * 100);
103
- const truncatedPackage = currentPackage.length > 40
104
- ? currentPackage.substring(0, 37) + '...'
105
- : currentPackage;
106
- this.showProgress(`🌐 Fetching ${percentage}% (${truncatedPackage})`);
94
+ : await (0, services_1.getAllPackageData)(packageNames, (_currentPackage, completed, total) => {
95
+ this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`);
107
96
  });
108
97
  try {
109
98
  for (const dep of allDeps) {
@@ -44,11 +44,8 @@ class UpgradeRunner {
44
44
  let shouldProceed = false;
45
45
  let previousSelections;
46
46
  while (true) {
47
- // Interactive selection (pass options for filtering)
48
- selectedChoices = await this.ui.selectPackagesToUpgrade(packages, previousSelections, {
49
- includePeerDeps: this.options?.includePeerDeps,
50
- includeOptionalDeps: this.options?.includeOptionalDeps,
51
- });
47
+ // Interactive selection
48
+ selectedChoices = await this.ui.selectPackagesToUpgrade(packages, previousSelections);
52
49
  if (selectedChoices.length === 0) {
53
50
  console.log(chalk_1.default.yellow('No packages selected. Exiting...'));
54
51
  return;
@@ -53,51 +53,14 @@ class InteractiveUI {
53
53
  async displayPackagesTable(packages) {
54
54
  console.log(this.renderer.renderPackagesTable(packages));
55
55
  }
56
- async selectPackagesToUpgrade(packages, previousSelections, options) {
56
+ async selectPackagesToUpgrade(packages, previousSelections) {
57
57
  const outdatedPackages = packages.filter((p) => p.isOutdated);
58
58
  if (outdatedPackages.length === 0) {
59
59
  return [];
60
60
  }
61
- // Filter packages based on CLI options.
62
- // IMPORTANT: This uses an opt-in approach where:
63
- // - Default (no flags): shows dependencies + devDependencies
64
- // - With -p flag: shows ONLY peerDependencies (excludes dependencies)
65
- // - With -o flag: shows ONLY optionalDependencies (excludes dependencies)
66
- // - With -p -o flags: shows peerDependencies + optionalDependencies (excludes dependencies)
67
- //
68
- // This design allows users to focus on one dependency type at a time,
69
- // which is useful since peer/optional deps have different upgrade semantics.
70
- let filteredPackages = outdatedPackages;
71
- let dependencyTypeLabel = '';
72
- if (options?.includePeerDeps || options?.includeOptionalDeps) {
73
- // If any special-case flag is provided, filter to show ONLY those types
74
- // (excluding regular dependencies/devDependencies)
75
- filteredPackages = outdatedPackages.filter((pkg) => {
76
- if (options.includePeerDeps && pkg.type === 'peerDependencies')
77
- return true;
78
- if (options.includeOptionalDeps && pkg.type === 'optionalDependencies')
79
- return true;
80
- return false;
81
- });
82
- // Build label describing which types are shown
83
- const types = [];
84
- if (options.includePeerDeps)
85
- types.push('Peer Deps');
86
- if (options.includeOptionalDeps)
87
- types.push('Optional Deps');
88
- dependencyTypeLabel = types.join(' & ');
89
- }
90
- else {
91
- // Default: show only regular dependencies and devDependencies
92
- filteredPackages = outdatedPackages.filter((pkg) => pkg.type === 'dependencies' || pkg.type === 'devDependencies');
93
- dependencyTypeLabel = 'Deps & Dev Deps';
94
- }
95
- if (filteredPackages.length === 0) {
96
- return [];
97
- }
98
61
  // Deduplicate packages by name and version specifier, but track all package.json paths
99
62
  const uniquePackages = new Map();
100
- for (const pkg of filteredPackages) {
63
+ for (const pkg of outdatedPackages) {
101
64
  const key = `${pkg.name}@${pkg.currentVersion}`;
102
65
  if (!uniquePackages.has(key)) {
103
66
  uniquePackages.set(key, {
@@ -150,7 +113,7 @@ class InteractiveUI {
150
113
  };
151
114
  });
152
115
  // Use custom interactive table selector (simplified - no grouping)
153
- const selectedStates = await this.interactiveTableSelector(selectionStates, dependencyTypeLabel);
116
+ const selectedStates = await this.interactiveTableSelector(selectionStates);
154
117
  // Convert to PackageUpgradeChoice[] - create one choice per package.json path
155
118
  const choices = [];
156
119
  selectedStates
@@ -179,7 +142,7 @@ class InteractiveUI {
179
142
  }
180
143
  return 24; // Fallback default
181
144
  }
182
- async interactiveTableSelector(selectionStates, dependencyTypeLabel) {
145
+ async interactiveTableSelector(selectionStates) {
183
146
  return new Promise((resolve) => {
184
147
  const states = [...selectionStates];
185
148
  const stateManager = new ui_1.StateManager(0, this.getTerminalHeight());
@@ -225,6 +188,11 @@ class InteractiveUI {
225
188
  stateManager.bulkUnselectAll(filteredStates);
226
189
  }
227
190
  break;
191
+ case 'toggle_dep_type_filter':
192
+ if (!uiState.showInfoModal && !uiState.showThemeModal) {
193
+ stateManager.toggleDependencyTypeFilter(action.depType);
194
+ }
195
+ break;
228
196
  case 'toggle_info_modal':
229
197
  if (!uiState.showInfoModal) {
230
198
  // Opening modal - load package info asynchronously
@@ -283,19 +251,17 @@ class InteractiveUI {
283
251
  case 'theme_navigate_up': {
284
252
  const themeManager = stateManager.getThemeManager();
285
253
  const currentIndex = themes_1.themeNames.indexOf(themeManager.getPreviewTheme());
286
- if (currentIndex > 0) {
287
- const themeNames = Object.keys(themes_1.themes);
288
- stateManager.previewTheme(themeNames[currentIndex - 1]);
289
- }
254
+ const themeArray = Object.keys(themes_1.themes);
255
+ const nextIndex = currentIndex > 0 ? currentIndex - 1 : themeArray.length - 1;
256
+ stateManager.previewTheme(themeArray[nextIndex]);
290
257
  break;
291
258
  }
292
259
  case 'theme_navigate_down': {
293
260
  const themeManager = stateManager.getThemeManager();
294
261
  const currentIndex = themes_1.themeNames.indexOf(themeManager.getPreviewTheme());
295
- if (currentIndex < themes_1.themeNames.length - 1) {
296
- const themeNames = Object.keys(themes_1.themes);
297
- stateManager.previewTheme(themeNames[currentIndex + 1]);
298
- }
262
+ const themeArray = Object.keys(themes_1.themes);
263
+ const nextIndex = currentIndex < themeArray.length - 1 ? currentIndex + 1 : 0;
264
+ stateManager.previewTheme(themeArray[nextIndex]);
299
265
  break;
300
266
  }
301
267
  case 'theme_confirm':
@@ -399,8 +365,9 @@ class InteractiveUI {
399
365
  else {
400
366
  // Normal list view (flat rendering - no grouping)
401
367
  const terminalWidth = process.stdout.columns || 80;
368
+ const activeFilterLabel = stateManager.getActiveFilterLabel();
402
369
  const lines = this.renderer.renderInterface(filteredStates, uiState.currentRow, uiState.scrollOffset, uiState.maxVisibleItems, uiState.forceFullRender, [], // No renderable items - use flat rendering
403
- dependencyTypeLabel, // Show which dependency type we're upgrading
370
+ activeFilterLabel, // Show current dependency type filter state
404
371
  this.packageManager, // Pass package manager info for header
405
372
  uiState.filterMode, uiState.filterQuery, states.length, terminalWidth);
406
373
  // Print all lines
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.changelogFetcher = exports.ChangelogFetcher = void 0;
4
+ const constants_1 = require("../config/constants");
4
5
  /**
5
6
  * Fetches package metadata from npm registry
6
7
  * Includes description, repository info, and basic metadata
@@ -69,12 +70,13 @@ class ChangelogFetcher {
69
70
  }
70
71
  }
71
72
  /**
72
- * Fetch data from npm registry
73
- * Returns the package data from the registry
73
+ * Fetch data from jsdelivr CDN
74
+ * Returns the package data by fetching package.json directly from jsdelivr
74
75
  */
75
76
  async fetchFromRegistry(packageName) {
76
77
  try {
77
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
78
+ // Fetch package.json directly from jsdelivr CDN (resolves to latest automatically)
79
+ const response = await fetch(`${constants_1.JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@latest/package.json`, {
78
80
  method: 'GET',
79
81
  headers: {
80
82
  accept: 'application/json',
@@ -83,20 +85,15 @@ class ChangelogFetcher {
83
85
  if (!response.ok) {
84
86
  return null;
85
87
  }
86
- const data = (await response.json());
87
- // Get the latest version data
88
- const distTags = data['dist-tags'];
89
- const latestVersion = distTags?.latest;
90
- const versions = data.versions;
91
- const latestPackageData = latestVersion ? versions?.[latestVersion] : undefined;
88
+ const pkgData = (await response.json());
92
89
  return {
93
- description: data.description,
94
- homepage: (data.homepage || latestPackageData?.homepage),
95
- repository: (data.repository || latestPackageData?.repository),
96
- bugs: (data.bugs || latestPackageData?.bugs),
97
- keywords: (data.keywords || []),
98
- author: (data.author || latestPackageData?.author),
99
- license: (data.license || latestPackageData?.license),
90
+ description: pkgData.description,
91
+ homepage: pkgData.homepage,
92
+ repository: pkgData.repository,
93
+ bugs: pkgData.bugs,
94
+ keywords: (pkgData.keywords || []),
95
+ author: pkgData.author,
96
+ license: pkgData.license,
100
97
  };
101
98
  }
102
99
  catch {
@@ -49,6 +49,9 @@ const jsdelivrPool = new undici_1.Pool('https://cdn.jsdelivr.net', {
49
49
  keepAliveMaxTimeout: config_1.REQUEST_TIMEOUT, // Maximum keep-alive timeout
50
50
  connectTimeout: config_1.REQUEST_TIMEOUT, // 60 seconds connect timeout
51
51
  });
52
+ // Batch configuration for progressive loading
53
+ const BATCH_SIZE = 5;
54
+ const BATCH_TIMEOUT_MS = 500;
52
55
  const packageCache = new Map();
53
56
  /**
54
57
  * Fetches package.json from jsdelivr CDN for a specific version tag using undici pool.
@@ -83,114 +86,149 @@ async function fetchPackageJsonFromJsdelivr(packageName, versionTag) {
83
86
  return null;
84
87
  }
85
88
  }
86
- /**
87
- * Fetches package data from jsdelivr CDN with fallback to npm registry.
88
- * Makes simultaneous requests for @latest, @major version, and current version.
89
- * @param packageName - The npm package name
90
- * @param currentVersion - The current version to extract major from (optional)
91
- * @returns Package data with latest version and all versions
92
- */
93
- async function fetchPackageFromJsdelivr(packageName, currentVersion) {
94
- // Check cache first
95
- const cached = packageCache.get(packageName);
96
- if (cached && Date.now() - cached.timestamp < config_1.CACHE_TTL) {
97
- return cached.data;
98
- }
99
- try {
100
- // Coerce the current version in case it's a range like ^1.1.5
101
- const coercedVersion = currentVersion ? semver.coerce(currentVersion)?.version : null;
102
- // Determine major version from current version if provided
103
- const majorVersion = currentVersion
104
- ? semver.major(semver.coerce(currentVersion) || '0.0.0').toString()
105
- : null;
106
- // Prepare requests: always fetch @latest, @major if we have a current version, and @currentVersion
107
- const requests = [
108
- fetchPackageJsonFromJsdelivr(packageName, 'latest'),
109
- ];
110
- if (majorVersion) {
111
- requests.push(fetchPackageJsonFromJsdelivr(packageName, majorVersion));
112
- }
113
- if (coercedVersion) {
114
- requests.push(fetchPackageJsonFromJsdelivr(packageName, coercedVersion));
115
- }
116
- // Execute all requests simultaneously
117
- const results = await Promise.all(requests);
118
- const latestResult = results[0];
119
- const majorResult = results[1];
120
- const currentResult = results[2];
121
- if (!latestResult) {
122
- // jsdelivr doesn't have this package, fallback to npm registry
123
- const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
124
- const data = npmData.get(packageName) || { latestVersion: 'unknown', allVersions: [] };
125
- // Cache the result
126
- packageCache.set(packageName, {
127
- data,
128
- timestamp: Date.now(),
129
- });
130
- return data;
131
- }
132
- const latestVersion = latestResult.version;
133
- const allVersions = [latestVersion];
134
- // Add the major version result if different from latest
135
- if (majorResult && majorResult.version !== latestVersion) {
136
- allVersions.push(majorResult.version);
137
- }
138
- // Add the current version result if different from latest and major
139
- if (currentResult && !allVersions.includes(currentResult.version)) {
140
- allVersions.push(currentResult.version);
141
- }
142
- const result = {
143
- latestVersion,
144
- allVersions: allVersions.sort(semver.rcompare),
145
- };
146
- // Cache the result
147
- packageCache.set(packageName, {
148
- data: result,
149
- timestamp: Date.now(),
150
- });
151
- return result;
152
- }
153
- catch (error) {
154
- // Fallback to npm registry on any error
155
- const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
156
- const data = npmData.get(packageName) || { latestVersion: 'unknown', allVersions: [] };
157
- // Cache the result
158
- packageCache.set(packageName, {
159
- data,
160
- timestamp: Date.now(),
161
- });
162
- return data;
163
- }
164
- }
165
89
  /**
166
90
  * Fetches package version data from jsdelivr CDN for multiple packages.
167
91
  * Uses undici connection pool for blazing fast performance with connection reuse.
168
- * Falls back to npm registry if jsdelivr doesn't have the package.
92
+ * Falls back to npm registry immediately when jsdelivr fails (interleaved, not sequential).
93
+ * Supports batched callbacks for progressive UI updates.
169
94
  * @param packageNames - Array of package names to fetch
170
95
  * @param currentVersions - Optional map of package names to their current versions
171
96
  * @param onProgress - Optional progress callback
97
+ * @param onBatchReady - Optional callback for batch updates (fires every BATCH_SIZE packages or BATCH_TIMEOUT_MS)
172
98
  * @returns Map of package names to their version data
173
99
  */
174
- async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onProgress) {
100
+ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onProgress, onBatchReady) {
175
101
  const packageData = new Map();
176
102
  if (packageNames.length === 0) {
177
103
  return packageData;
178
104
  }
179
105
  const total = packageNames.length;
180
106
  let completedCount = 0;
181
- // Fire all requests simultaneously - undici pool handles concurrency internally
182
- // No need for p-limit - the pool's connection limit controls concurrency
183
- const allPromises = packageNames.map(async (packageName) => {
107
+ // Batch buffer for progressive updates
108
+ let batchBuffer = [];
109
+ let batchTimer = null;
110
+ // Helper to flush the current batch
111
+ const flushBatch = () => {
112
+ if (batchBuffer.length > 0 && onBatchReady) {
113
+ onBatchReady([...batchBuffer]);
114
+ batchBuffer = [];
115
+ }
116
+ if (batchTimer) {
117
+ clearTimeout(batchTimer);
118
+ batchTimer = null;
119
+ }
120
+ };
121
+ // Helper to add package to batch and flush if needed
122
+ const addToBatch = (packageName, data) => {
123
+ if (onBatchReady) {
124
+ batchBuffer.push({ name: packageName, data });
125
+ // Flush if batch is full
126
+ if (batchBuffer.length >= BATCH_SIZE) {
127
+ flushBatch();
128
+ }
129
+ else if (!batchTimer) {
130
+ // Set timer to flush batch after timeout
131
+ batchTimer = setTimeout(flushBatch, BATCH_TIMEOUT_MS);
132
+ }
133
+ }
134
+ };
135
+ // Process individual package fetch with immediate npm fallback on failure
136
+ const fetchPackageWithFallback = async (packageName) => {
184
137
  const currentVersion = currentVersions?.get(packageName);
185
- const data = await fetchPackageFromJsdelivr(packageName, currentVersion);
186
- packageData.set(packageName, data);
187
- completedCount++;
188
- if (onProgress) {
189
- onProgress(packageName, completedCount, total);
138
+ // Try to get from cache first
139
+ const cached = packageCache.get(packageName);
140
+ if (cached && Date.now() - cached.timestamp < config_1.CACHE_TTL) {
141
+ packageData.set(packageName, cached.data);
142
+ completedCount++;
143
+ if (onProgress) {
144
+ onProgress(packageName, completedCount, total);
145
+ }
146
+ addToBatch(packageName, cached.data);
147
+ return;
148
+ }
149
+ try {
150
+ // Determine major version from current version if provided
151
+ const majorVersion = currentVersion
152
+ ? semver.major(semver.coerce(currentVersion) || '0.0.0').toString()
153
+ : null;
154
+ // Prepare requests: always fetch @latest, @major if we have a current version
155
+ const requests = [
156
+ fetchPackageJsonFromJsdelivr(packageName, 'latest'),
157
+ ];
158
+ if (majorVersion) {
159
+ requests.push(fetchPackageJsonFromJsdelivr(packageName, majorVersion));
160
+ }
161
+ // Execute all requests simultaneously
162
+ const results = await Promise.all(requests);
163
+ const latestResult = results[0];
164
+ const majorResult = results[1];
165
+ if (!latestResult) {
166
+ // Package not on jsDelivr, immediately try npm fallback
167
+ const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
168
+ const result = npmData.get(packageName);
169
+ if (result) {
170
+ packageData.set(packageName, result);
171
+ packageCache.set(packageName, {
172
+ data: result,
173
+ timestamp: Date.now(),
174
+ });
175
+ addToBatch(packageName, result);
176
+ }
177
+ completedCount++;
178
+ if (onProgress) {
179
+ onProgress(packageName, completedCount, total);
180
+ }
181
+ return;
182
+ }
183
+ const latestVersion = latestResult.version;
184
+ const allVersions = [latestVersion];
185
+ // Add the major version result if different from latest
186
+ if (majorResult && majorResult.version !== latestVersion) {
187
+ allVersions.push(majorResult.version);
188
+ }
189
+ const result = {
190
+ latestVersion,
191
+ allVersions: allVersions.sort(semver.rcompare),
192
+ };
193
+ // Cache the result
194
+ packageCache.set(packageName, {
195
+ data: result,
196
+ timestamp: Date.now(),
197
+ });
198
+ packageData.set(packageName, result);
199
+ completedCount++;
200
+ if (onProgress) {
201
+ onProgress(packageName, completedCount, total);
202
+ }
203
+ addToBatch(packageName, result);
190
204
  }
191
- });
192
- // Wait for all requests to complete
193
- await Promise.all(allPromises);
205
+ catch (error) {
206
+ // On error, immediately try npm fallback
207
+ try {
208
+ const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
209
+ const result = npmData.get(packageName);
210
+ if (result) {
211
+ packageData.set(packageName, result);
212
+ packageCache.set(packageName, {
213
+ data: result,
214
+ timestamp: Date.now(),
215
+ });
216
+ addToBatch(packageName, result);
217
+ }
218
+ }
219
+ catch (npmError) {
220
+ // If both fail, just continue
221
+ }
222
+ completedCount++;
223
+ if (onProgress) {
224
+ onProgress(packageName, completedCount, total);
225
+ }
226
+ }
227
+ };
228
+ // Fire all requests simultaneously - they handle fallback internally and immediately
229
+ await Promise.all(packageNames.map(fetchPackageWithFallback));
230
+ // Flush any remaining batch items
231
+ flushBatch();
194
232
  // Clear the progress line and show completion time if no custom progress handler
195
233
  if (!onProgress) {
196
234
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
@@ -24,6 +24,7 @@ class InputHandler {
24
24
  if (key) {
25
25
  switch (key.name) {
26
26
  case 'escape':
27
+ // Close theme modal (which also resets theme on cancel)
27
28
  this.onAction({ type: 'toggle_theme_modal' });
28
29
  return;
29
30
  case 'return':
@@ -35,6 +36,11 @@ class InputHandler {
35
36
  case 'down':
36
37
  this.onAction({ type: 'theme_navigate_down' });
37
38
  return;
39
+ case 't':
40
+ case 'T':
41
+ // Allow 't' to toggle theme modal closed as well
42
+ this.onAction({ type: 'toggle_theme_modal' });
43
+ return;
38
44
  default:
39
45
  return;
40
46
  }
@@ -131,6 +137,24 @@ class InputHandler {
131
137
  case 'U':
132
138
  this.onAction({ type: 'bulk_unselect_all' });
133
139
  break;
140
+ case 'd':
141
+ case 'D':
142
+ if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
143
+ this.onAction({ type: 'toggle_dep_type_filter', depType: 'devDependencies' });
144
+ }
145
+ break;
146
+ case 'p':
147
+ case 'P':
148
+ if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
149
+ this.onAction({ type: 'toggle_dep_type_filter', depType: 'peerDependencies' });
150
+ }
151
+ break;
152
+ case 'o':
153
+ case 'O':
154
+ if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
155
+ this.onAction({ type: 'toggle_dep_type_filter', depType: 'optionalDependencies' });
156
+ }
157
+ break;
134
158
  case 'i':
135
159
  case 'I':
136
160
  this.onAction({ type: 'toggle_info_modal' });
@@ -54,8 +54,8 @@ class UIRenderer {
54
54
  renderSpacer() {
55
55
  return PackageList.renderSpacer();
56
56
  }
57
- renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, dependencyTypeLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth = 80) {
58
- return PackageList.renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, dependencyTypeLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth);
57
+ renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, activeFilterLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth = 80) {
58
+ return PackageList.renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, activeFilterLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth);
59
59
  }
60
60
  renderPackagesTable(packages) {
61
61
  return PackageList.renderPackagesTable(packages);
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderPackageInfoLoading = renderPackageInfoLoading;
7
7
  exports.renderPackageInfoModal = renderPackageInfoModal;
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
+ const themes_colors_1 = require("../themes-colors");
9
10
  /**
10
11
  * Remove ANSI color codes from a string for length calculation
11
12
  */
@@ -138,7 +139,7 @@ function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24)
138
139
  lines.push(' '.repeat(padding) +
139
140
  chalk_1.default.gray('│') +
140
141
  ' ' +
141
- chalk_1.default.blue(downloadsText) +
142
+ (0, themes_colors_1.getThemeColor)('primary')(downloadsText) +
142
143
  ' '.repeat(downloadsPadding) +
143
144
  chalk_1.default.gray('│'));
144
145
  }
@@ -162,20 +163,28 @@ function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24)
162
163
  lines.push(' '.repeat(padding) + chalk_1.default.gray('├' + '─'.repeat(modalWidth - 2) + '┤'));
163
164
  const repoLabel = 'Changelog:';
164
165
  const repoUrl = state.repository.substring(0, modalWidth - 20);
165
- const repoText = ` ${repoLabel} ${chalk_1.default.blue.underline(repoUrl)}`;
166
+ const repoText = ` ${repoLabel} ${chalk_1.default.underline((0, themes_colors_1.getThemeColor)('primary')(repoUrl))}`;
166
167
  const repoLength = stripAnsi(repoText).length;
167
- const repoPadding = Math.max(0, modalWidth - 2 - repoLength);
168
- lines.push(' '.repeat(padding) + chalk_1.default.gray('│') + repoText + ' '.repeat(repoPadding) + chalk_1.default.gray('│'));
168
+ const repoPadding = Math.max(0, modalWidth - 3 - repoLength);
169
+ lines.push(' '.repeat(padding) +
170
+ chalk_1.default.gray('│') +
171
+ repoText +
172
+ ' '.repeat(repoPadding) +
173
+ chalk_1.default.gray('│'));
169
174
  }
170
175
  // Links section
171
176
  if (state.homepage) {
172
177
  lines.push(' '.repeat(padding) + chalk_1.default.gray('├' + '─'.repeat(modalWidth - 2) + '┤'));
173
178
  const homeLabel = 'Homepage:';
174
179
  const homeUrl = state.homepage.substring(0, modalWidth - 20);
175
- const homeText = ` ${homeLabel} ${chalk_1.default.blue.underline(homeUrl)}`;
180
+ const homeText = ` ${homeLabel} ${chalk_1.default.underline((0, themes_colors_1.getThemeColor)('primary')(homeUrl))}`;
176
181
  const homeLength = stripAnsi(homeText).length;
177
- const homePadding = Math.max(0, modalWidth - 2 - homeLength);
178
- lines.push(' '.repeat(padding) + chalk_1.default.gray('│') + homeText + ' '.repeat(homePadding) + chalk_1.default.gray('│'));
182
+ const homePadding = Math.max(0, modalWidth - 3 - homeLength);
183
+ lines.push(' '.repeat(padding) +
184
+ chalk_1.default.gray('│') +
185
+ homeText +
186
+ ' '.repeat(homePadding) +
187
+ chalk_1.default.gray('│'));
179
188
  }
180
189
  // Footer
181
190
  lines.push(' '.repeat(padding) + chalk_1.default.gray('╰' + '─'.repeat(modalWidth - 2) + '╯'));
@@ -11,6 +11,22 @@ exports.renderPackagesTable = renderPackagesTable;
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
12
  const utils_1 = require("../utils");
13
13
  const themes_colors_1 = require("../themes-colors");
14
+ /**
15
+ * Get type badge for dependency type (theme-aware)
16
+ */
17
+ function getTypeBadge(type) {
18
+ switch (type) {
19
+ case 'devDependencies':
20
+ return (0, themes_colors_1.getThemeColor)('textSecondary')('[D]');
21
+ case 'peerDependencies':
22
+ return (0, themes_colors_1.getThemeColor)('textSecondary')('[P]');
23
+ case 'optionalDependencies':
24
+ return (0, themes_colors_1.getThemeColor)('textSecondary')('[O]');
25
+ case 'dependencies':
26
+ default:
27
+ return ''; // No badge for regular dependencies
28
+ }
29
+ }
14
30
  /**
15
31
  * Render a single package line
16
32
  * @param state Package selection state
@@ -86,18 +102,23 @@ function renderPackageLine(state, index, isCurrentRow, terminalWidth = 80) {
86
102
  const availableForPackageName = terminalWidth - prefixWidth - otherColumnsWidth - 1;
87
103
  const packageNameWidth = Math.min(maxPackageNameWidth, Math.max(minPackageNameWidth, availableForPackageName));
88
104
  // Apply ellipsis truncation if package name exceeds available width
89
- const truncatedName = utils_1.VersionUtils.truncateMiddle(state.name, packageNameWidth - 1); // -1 for space after name
105
+ const badgeWidth = state.type === 'dependencies' ? 0 : 3; // [X] without leading space
106
+ const truncatedName = utils_1.VersionUtils.truncateMiddle(state.name, packageNameWidth - 1 - badgeWidth); // -1 for space after name, -badgeWidth for badge
90
107
  // Helper function to determine if dashes should be shown based on available padding
91
108
  // Only show dashes if there's significant padding (> 2 chars) to fill
92
109
  const shouldShowDashes = (paddingAmount) => paddingAmount > 2;
93
110
  const dashColor = isCurrentRow ? chalk_1.default.white : chalk_1.default.gray;
94
- // Package name with dashes only if needed
95
- const nameLength = utils_1.VersionUtils.getVisualLength(truncatedName);
96
- const namePadding = Math.max(0, packageNameWidth - nameLength - 1); // -1 for space after package name
97
- const nameDashes = shouldShowDashes(namePadding) ? dashColor('-').repeat(namePadding) : ' '.repeat(namePadding);
98
111
  // Use truncated name if it differs from original, otherwise use colored packageName
99
112
  const displayName = truncatedName !== state.name ? truncatedName : packageName;
100
- const packageNameSection = `${displayName} ${nameDashes}`;
113
+ // Package name with dashes and badge at the end
114
+ const typeBadge = getTypeBadge(state.type);
115
+ const nameLength = utils_1.VersionUtils.getVisualLength(truncatedName);
116
+ const namePadding = Math.max(0, packageNameWidth - nameLength - 1 - badgeWidth); // -1 for space after package name, -badgeWidth for badge at end
117
+ const nameDashes = shouldShowDashes(namePadding) ? dashColor('-').repeat(namePadding) : ' '.repeat(namePadding);
118
+ // Place badge at the end of dashes: name ------[D]
119
+ const packageNameSection = typeBadge
120
+ ? `${displayName} ${nameDashes}${typeBadge}`
121
+ : `${displayName} ${nameDashes}`;
101
122
  // Current version section with dashes only if needed
102
123
  const currentSection = `${currentDot} ${currentVersion}`;
103
124
  const currentSectionLength = utils_1.VersionUtils.getVisualLength(currentSection) + 1; // +1 for space before padding
@@ -150,7 +171,7 @@ function renderSpacer() {
150
171
  /**
151
172
  * Render the main interface
152
173
  */
153
- function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, dependencyTypeLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth = 80) {
174
+ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forceFullRender, renderableItems, activeFilterLabel, packageManager, filterMode, filterQuery, totalPackagesBeforeFilter, terminalWidth = 80) {
154
175
  const output = [];
155
176
  // Header section (same for initial and incremental render)
156
177
  if (packageManager) {
@@ -166,17 +187,31 @@ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forc
166
187
  const inupColors = [chalk_1.default.red, chalk_1.default.yellow, chalk_1.default.blue, chalk_1.default.magenta];
167
188
  const coloredInup = inupColors.map((color, i) => color.bold('inup'[i])).join('');
168
189
  const headerLine = ' ' + chalk_1.default.bold(pmColor('🚀')) + ' ' + coloredInup + (0, themes_colors_1.getThemeColor)('textSecondary')(` (${packageManager.displayName})`);
169
- output.push(dependencyTypeLabel ? headerLine + (0, themes_colors_1.getThemeColor)('textSecondary')(' - ') + (0, themes_colors_1.getThemeColor)('primary')(dependencyTypeLabel) : headerLine);
190
+ // Show filter state (always show, including "All")
191
+ const fullHeaderLine = activeFilterLabel
192
+ ? headerLine + (0, themes_colors_1.getThemeColor)('textSecondary')(' - ') + (0, themes_colors_1.getThemeColor)('primary')(activeFilterLabel)
193
+ : headerLine;
194
+ // Pad to terminal width to clear any leftover characters
195
+ const headerPadding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(fullHeaderLine));
196
+ output.push(fullHeaderLine + ' '.repeat(headerPadding));
170
197
  }
171
198
  else {
172
199
  const headerLine = ' ' + chalk_1.default.bold.blue('🚀 ') + chalk_1.default.bold.red('i') + chalk_1.default.bold.yellow('n') + chalk_1.default.bold.blue('u') + chalk_1.default.bold.magenta('p');
173
- output.push(dependencyTypeLabel ? headerLine + (0, themes_colors_1.getThemeColor)('textSecondary')(' - ') + (0, themes_colors_1.getThemeColor)('primary')(dependencyTypeLabel) : headerLine);
200
+ // Show filter state (always show, including "All")
201
+ const fullHeaderLine = activeFilterLabel
202
+ ? headerLine + (0, themes_colors_1.getThemeColor)('textSecondary')(' - ') + (0, themes_colors_1.getThemeColor)('primary')(activeFilterLabel)
203
+ : headerLine;
204
+ // Pad to terminal width to clear any leftover characters
205
+ const headerPadding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(fullHeaderLine));
206
+ output.push(fullHeaderLine + ' '.repeat(headerPadding));
174
207
  }
175
208
  output.push('');
176
209
  if (filterMode) {
177
210
  // Show filter input with cursor when actively filtering
178
211
  const filterDisplay = ' ' + chalk_1.default.bold.white('Search: ') + (0, themes_colors_1.getThemeColor)('primary')(filterQuery || '') + (0, themes_colors_1.getThemeColor)('border')('█');
179
- output.push(filterDisplay);
212
+ // Pad to terminal width to clear any leftover characters from backspace
213
+ const padding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(filterDisplay));
214
+ output.push(filterDisplay + ' '.repeat(padding));
180
215
  }
181
216
  else {
182
217
  // Show instructions when not filtering
@@ -190,12 +225,12 @@ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forc
190
225
  chalk_1.default.bold.white('←/→ ') +
191
226
  (0, themes_colors_1.getThemeColor)('textSecondary')('Select') +
192
227
  ' ' +
228
+ chalk_1.default.bold.white('D/P/O ') +
229
+ (0, themes_colors_1.getThemeColor)('textSecondary')('Filter') +
230
+ ' ' +
193
231
  chalk_1.default.bold.white('I ') +
194
232
  (0, themes_colors_1.getThemeColor)('textSecondary')('Info') +
195
233
  ' ' +
196
- chalk_1.default.bold.white('T ') +
197
- (0, themes_colors_1.getThemeColor)('textSecondary')('Theme') +
198
- ' ' +
199
234
  chalk_1.default.bold.white('M ') +
200
235
  (0, themes_colors_1.getThemeColor)('textSecondary')('Minor') +
201
236
  ' ' +
@@ -6,6 +6,10 @@ class FilterManager {
6
6
  this.state = {
7
7
  filterMode: false,
8
8
  filterQuery: '',
9
+ showDependencies: true,
10
+ showDevDependencies: true,
11
+ showPeerDependencies: true,
12
+ showOptionalDependencies: true,
9
13
  };
10
14
  }
11
15
  getState() {
@@ -36,12 +40,59 @@ class FilterManager {
36
40
  this.state.filterQuery = this.state.filterQuery.slice(0, -1);
37
41
  }
38
42
  }
43
+ toggleDependencyType(type) {
44
+ switch (type) {
45
+ case 'dependencies':
46
+ this.state.showDependencies = !this.state.showDependencies;
47
+ break;
48
+ case 'devDependencies':
49
+ this.state.showDevDependencies = !this.state.showDevDependencies;
50
+ break;
51
+ case 'peerDependencies':
52
+ this.state.showPeerDependencies = !this.state.showPeerDependencies;
53
+ break;
54
+ case 'optionalDependencies':
55
+ this.state.showOptionalDependencies = !this.state.showOptionalDependencies;
56
+ break;
57
+ }
58
+ }
59
+ getActiveFilterLabel() {
60
+ const activeTypes = [];
61
+ if (this.state.showDependencies)
62
+ activeTypes.push('Deps');
63
+ if (this.state.showDevDependencies)
64
+ activeTypes.push('Dev');
65
+ if (this.state.showPeerDependencies)
66
+ activeTypes.push('Peer');
67
+ if (this.state.showOptionalDependencies)
68
+ activeTypes.push('Optional');
69
+ if (activeTypes.length === 0)
70
+ return 'None';
71
+ return activeTypes.join(', ');
72
+ }
39
73
  getFilteredStates(allStates) {
40
- if (!this.state.filterQuery) {
41
- return allStates;
74
+ let filtered = allStates;
75
+ // Apply text filter
76
+ if (this.state.filterQuery) {
77
+ const query = this.state.filterQuery.toLowerCase();
78
+ filtered = filtered.filter((state) => state.name.toLowerCase().includes(query));
42
79
  }
43
- const query = this.state.filterQuery.toLowerCase();
44
- return allStates.filter((state) => state.name.toLowerCase().includes(query));
80
+ // Apply dependency type filter
81
+ filtered = filtered.filter((state) => {
82
+ switch (state.type) {
83
+ case 'dependencies':
84
+ return this.state.showDependencies;
85
+ case 'devDependencies':
86
+ return this.state.showDevDependencies;
87
+ case 'peerDependencies':
88
+ return this.state.showPeerDependencies;
89
+ case 'optionalDependencies':
90
+ return this.state.showOptionalDependencies;
91
+ default:
92
+ return true;
93
+ }
94
+ });
95
+ return filtered;
45
96
  }
46
97
  }
47
98
  exports.FilterManager = FilterManager;
@@ -163,13 +163,13 @@ class StateManager {
163
163
  // Filter delegation
164
164
  enterFilterMode() {
165
165
  this.filterManager.enterFilterMode();
166
- this.renderState.forceFullRender = true;
166
+ // Use incremental render for search mode toggle (no blink)
167
167
  }
168
168
  exitFilterMode() {
169
169
  this.filterManager.exitFilterMode();
170
170
  this.navigationManager.setCurrentRow(0);
171
171
  this.navigationManager.setScrollOffset(0);
172
- this.renderState.forceFullRender = true;
172
+ // Use incremental render for search mode toggle (no blink)
173
173
  }
174
174
  updateFilterQuery(query) {
175
175
  this.filterManager.updateFilterQuery(query);
@@ -180,17 +180,25 @@ class StateManager {
180
180
  this.filterManager.appendToFilterQuery(char);
181
181
  this.navigationManager.setCurrentRow(0);
182
182
  this.navigationManager.setScrollOffset(0);
183
- this.renderState.forceFullRender = true;
184
183
  }
185
184
  deleteFromFilterQuery() {
186
185
  this.filterManager.deleteFromFilterQuery();
187
186
  this.navigationManager.setCurrentRow(0);
188
187
  this.navigationManager.setScrollOffset(0);
189
- this.renderState.forceFullRender = true;
190
188
  }
191
189
  getFilteredStates(allStates) {
192
190
  return this.filterManager.getFilteredStates(allStates);
193
191
  }
192
+ toggleDependencyTypeFilter(type) {
193
+ this.filterManager.toggleDependencyType(type);
194
+ // Reset navigation when filter changes
195
+ this.navigationManager.setCurrentRow(0);
196
+ this.navigationManager.setScrollOffset(0);
197
+ // Use incremental render (no blink)
198
+ }
199
+ getActiveFilterLabel() {
200
+ return this.filterManager.getActiveFilterLabel();
201
+ }
194
202
  // Display and render state management
195
203
  updateTerminalHeight(newHeight) {
196
204
  const newMaxVisibleItems = Math.max(5, newHeight - this.headerLines - 2);
@@ -40,6 +40,8 @@ class ThemeManager {
40
40
  this.state.showThemeModal = false;
41
41
  // Reset preview to current theme when closing without confirmation
42
42
  this.state.previewTheme = this.state.currentTheme;
43
+ // Restore global theme to confirmed theme when canceling
44
+ globalCurrentTheme = this.state.currentTheme;
43
45
  }
44
46
  previewTheme(themeName) {
45
47
  if (themes_1.themeNames.includes(themeName)) {
@@ -24,7 +24,7 @@ const themeColorDefinitions = {
24
24
  textSecondary: 'gray',
25
25
  },
26
26
  dracula: {
27
- bg: '#282A36',
27
+ bg: '#1e1f26',
28
28
  primary: '#8BE9FD',
29
29
  secondary: '#FF79C6',
30
30
  success: '#50FA7B',
@@ -78,6 +78,17 @@ const themeColorDefinitions = {
78
78
  text: '#C0CAF5',
79
79
  textSecondary: '#A9B1D6',
80
80
  },
81
+ onedark: {
82
+ bg: '#282c34',
83
+ primary: '#61AFEF',
84
+ secondary: '#C678DD',
85
+ success: '#98C379',
86
+ warning: '#E5C07B',
87
+ error: '#E06C75',
88
+ border: '#3E4452',
89
+ text: '#ABB2BF',
90
+ textSecondary: '#5C6370',
91
+ },
81
92
  };
82
93
  // Helper to apply color - handles both hex and named colors
83
94
  function applyColor(color, text) {
@@ -61,18 +61,11 @@ async function readPackageJsonAsync(path) {
61
61
  * Optionally includes peer and optional dependencies based on flags.
62
62
  */
63
63
  function collectAllDependencies(packageJsonFiles, options = {}) {
64
- const { includePeerDeps = false, includeOptionalDeps = false } = options;
65
64
  const allDeps = [];
66
65
  for (const packageJsonPath of packageJsonFiles) {
67
66
  try {
68
67
  const packageJson = readPackageJson(packageJsonPath);
69
- const depTypes = ['dependencies', 'devDependencies'];
70
- if (includeOptionalDeps) {
71
- depTypes.push('optionalDependencies');
72
- }
73
- if (includePeerDeps) {
74
- depTypes.push('peerDependencies');
75
- }
68
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
76
69
  for (const depType of depTypes) {
77
70
  const deps = packageJson[depType];
78
71
  if (deps && typeof deps === 'object') {
@@ -100,7 +93,6 @@ function collectAllDependencies(packageJsonFiles, options = {}) {
100
93
  * Optionally includes peer and optional dependencies based on flags.
101
94
  */
102
95
  async function collectAllDependenciesAsync(packageJsonFiles, options = {}) {
103
- const { includePeerDeps = false, includeOptionalDeps = false } = options;
104
96
  // Read all package.json files in parallel
105
97
  const packageJsonPromises = packageJsonFiles.map(async (packageJsonPath) => {
106
98
  try {
@@ -119,13 +111,7 @@ async function collectAllDependenciesAsync(packageJsonFiles, options = {}) {
119
111
  if (!result)
120
112
  continue;
121
113
  const { packageJson, packageJsonPath } = result;
122
- const depTypes = ['dependencies', 'devDependencies'];
123
- if (includeOptionalDeps) {
124
- depTypes.push('optionalDependencies');
125
- }
126
- if (includePeerDeps) {
127
- depTypes.push('peerDependencies');
128
- }
114
+ const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
129
115
  for (const depType of depTypes) {
130
116
  const deps = packageJson[depType];
131
117
  if (deps && typeof deps === 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
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.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,11 +37,13 @@
37
37
  ],
38
38
  "devDependencies": {
39
39
  "@types/inquirer": "^9.0.9",
40
- "@types/keypress": "^2.0.30",
40
+ "@types/keypress.js": "^2.1.3",
41
41
  "@types/node": "^24.10.1",
42
42
  "@types/semver": "^7.7.1",
43
+ "@vitest/coverage-v8": "^3.0.0",
43
44
  "prettier": "^3.8.0",
44
- "typescript": "^5.9.3"
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^3.0.0"
45
47
  },
46
48
  "dependencies": {
47
49
  "chalk": "^5.6.2",
@@ -67,6 +69,9 @@
67
69
  "format": "prettier --write src/**/*.ts",
68
70
  "format:check": "prettier --check src/**/*.ts",
69
71
  "demo:record": "bash docs/demo/record-demo.sh",
70
- "demo:setup": "cd docs/demo-project && pnpm install"
72
+ "demo:setup": "cd docs/demo-project && pnpm install",
73
+ "test": "vitest run",
74
+ "test:watch": "vitest",
75
+ "test:coverage": "vitest run --coverage"
71
76
  }
72
77
  }