inup 1.4.4 → 1.4.6

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/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ const fs_1 = require("fs");
10
10
  const path_1 = require("path");
11
11
  const index_1 = require("./index");
12
12
  const services_1 = require("./services");
13
+ const config_1 = require("./config");
13
14
  const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../package.json'), 'utf-8'));
14
15
  const program = new commander_1.Command();
15
16
  program
@@ -18,17 +19,31 @@ program
18
19
  .version(packageJson.version)
19
20
  .option('-d, --dir <directory>', 'specify directory to run in', process.cwd())
20
21
  .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
22
+ .option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
21
23
  .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
22
24
  .action(async (options) => {
23
25
  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`);
24
26
  // Check for updates in the background (non-blocking)
25
27
  const updateCheckPromise = (0, services_1.checkForUpdateAsync)('inup', packageJson.version);
26
- const excludePatterns = options.exclude
28
+ const cwd = (0, path_1.resolve)(options.dir);
29
+ // Load project config from .inuprc
30
+ const projectConfig = (0, config_1.loadProjectConfig)(cwd);
31
+ // Merge CLI exclude patterns with config
32
+ const cliExcludePatterns = options.exclude
27
33
  ? options.exclude
28
34
  .split(',')
29
35
  .map((p) => p.trim())
30
36
  .filter(Boolean)
31
37
  : [];
38
+ const excludePatterns = [...cliExcludePatterns, ...(projectConfig.exclude || [])];
39
+ // Merge CLI ignore patterns with config (CLI takes precedence / adds to config)
40
+ const cliIgnorePatterns = options.ignore
41
+ ? options.ignore
42
+ .split(',')
43
+ .map((p) => p.trim())
44
+ .filter(Boolean)
45
+ : [];
46
+ const ignorePackages = [...new Set([...cliIgnorePatterns, ...(projectConfig.ignore || [])])];
32
47
  // Validate package manager if provided
33
48
  let packageManager;
34
49
  if (options.packageManager) {
@@ -41,8 +56,9 @@ program
41
56
  packageManager = options.packageManager;
42
57
  }
43
58
  const upgrader = new index_1.UpgradeRunner({
44
- cwd: options.dir,
59
+ cwd,
45
60
  excludePatterns,
61
+ ignorePackages,
46
62
  packageManager,
47
63
  });
48
64
  await upgrader.run();
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./constants"), exports);
18
+ __exportStar(require("./project-config"), exports);
18
19
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadProjectConfig = loadProjectConfig;
4
+ exports.isPackageIgnored = isPackageIgnored;
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const CONFIG_FILES = ['.inuprc', '.inuprc.json', 'inup.config.json'];
8
+ /**
9
+ * Load project configuration from .inuprc, .inuprc.json, or inup.config.json
10
+ * Searches in the specified directory and parent directories up to root
11
+ */
12
+ function loadProjectConfig(cwd) {
13
+ let currentDir = cwd;
14
+ while (currentDir !== '/') {
15
+ for (const configFile of CONFIG_FILES) {
16
+ const configPath = (0, path_1.join)(currentDir, configFile);
17
+ if ((0, fs_1.existsSync)(configPath)) {
18
+ try {
19
+ const content = (0, fs_1.readFileSync)(configPath, 'utf-8');
20
+ const config = JSON.parse(content);
21
+ return normalizeConfig(config);
22
+ }
23
+ catch (error) {
24
+ // Invalid JSON or read error - continue searching
25
+ console.warn(`Warning: Failed to parse ${configPath}: ${error}`);
26
+ }
27
+ }
28
+ }
29
+ // Move to parent directory
30
+ const parentDir = (0, path_1.join)(currentDir, '..');
31
+ if (parentDir === currentDir)
32
+ break;
33
+ currentDir = parentDir;
34
+ }
35
+ return {};
36
+ }
37
+ /**
38
+ * Normalize and validate the config
39
+ */
40
+ function normalizeConfig(config) {
41
+ const normalized = {};
42
+ if (config.ignore) {
43
+ if (Array.isArray(config.ignore)) {
44
+ normalized.ignore = config.ignore.filter((item) => typeof item === 'string');
45
+ }
46
+ }
47
+ if (config.exclude) {
48
+ if (Array.isArray(config.exclude)) {
49
+ normalized.exclude = config.exclude.filter((item) => typeof item === 'string');
50
+ }
51
+ }
52
+ return normalized;
53
+ }
54
+ /**
55
+ * Check if a package name matches any of the ignore patterns
56
+ * Supports exact matches and glob patterns (* and ?)
57
+ */
58
+ function isPackageIgnored(packageName, ignorePatterns) {
59
+ for (const pattern of ignorePatterns) {
60
+ if (matchesPattern(packageName, pattern)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ /**
67
+ * Match a package name against a pattern
68
+ * Supports:
69
+ * - Exact match: "lodash"
70
+ * - Wildcard: "*" matches any sequence of characters
71
+ * - Single char wildcard: "?" matches single character
72
+ * - Scoped packages: "@babel/*" matches all @babel packages
73
+ */
74
+ function matchesPattern(name, pattern) {
75
+ // Exact match
76
+ if (pattern === name) {
77
+ return true;
78
+ }
79
+ // Convert glob pattern to regex
80
+ const regexPattern = pattern
81
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except * and ?
82
+ .replace(/\*/g, '.*') // * matches any sequence
83
+ .replace(/\?/g, '.'); // ? matches single char
84
+ const regex = new RegExp(`^${regexPattern}$`);
85
+ return regex.test(name);
86
+ }
87
+ //# sourceMappingURL=project-config.js.map
@@ -38,12 +38,14 @@ const semver = __importStar(require("semver"));
38
38
  const utils_1 = require("../utils");
39
39
  const services_1 = require("../services");
40
40
  const config_1 = require("../config");
41
+ const utils_2 = require("../ui/utils");
41
42
  class PackageDetector {
42
43
  constructor(options) {
43
44
  this.packageJsonPath = null;
44
45
  this.packageJson = null;
45
46
  this.cwd = options?.cwd || process.cwd();
46
47
  this.excludePatterns = options?.excludePatterns || [];
48
+ this.ignorePackages = options?.ignorePackages || [];
47
49
  this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
48
50
  if (this.packageJsonPath) {
49
51
  this.packageJson = (0, utils_1.readPackageJson)(this.packageJsonPath);
@@ -67,15 +69,24 @@ class PackageDetector {
67
69
  includePeerDeps: true,
68
70
  includeOptionalDeps: true,
69
71
  });
70
- // Step 3: Get unique package names while filtering out workspace references
72
+ // Step 3: Get unique package names while filtering out workspace references and ignored packages
71
73
  this.showProgress('🔍 Identifying unique packages...');
72
74
  const uniquePackageNames = new Set();
73
75
  const allDeps = [];
76
+ let ignoredCount = 0;
74
77
  for (const dep of allDepsRaw) {
75
- if (!this.isWorkspaceReference(dep.version)) {
76
- allDeps.push(dep);
77
- uniquePackageNames.add(dep.name);
78
+ if (this.isWorkspaceReference(dep.version)) {
79
+ continue;
78
80
  }
81
+ if (this.ignorePackages.length > 0 && (0, config_1.isPackageIgnored)(dep.name, this.ignorePackages)) {
82
+ ignoredCount++;
83
+ continue;
84
+ }
85
+ allDeps.push(dep);
86
+ uniquePackageNames.add(dep.name);
87
+ }
88
+ if (ignoredCount > 0) {
89
+ this.showProgress(`🔍 Skipped ${ignoredCount} ignored package(s)`);
79
90
  }
80
91
  const packageNames = Array.from(uniquePackageNames);
81
92
  // Step 4: Fetch all package data in one call per package
@@ -171,8 +182,7 @@ class PackageDetector {
171
182
  version.startsWith('bitbucket:'));
172
183
  }
173
184
  showProgress(message) {
174
- // Clear current line and show new message
175
- process.stdout.write(`\r${' '.repeat(80)}\r${message}`);
185
+ utils_2.ConsoleUtils.showProgress(message);
176
186
  }
177
187
  getOutdatedPackagesOnly(packages) {
178
188
  return packages.filter((pkg) => pkg.isOutdated);
@@ -221,10 +221,10 @@ class InteractiveUI {
221
221
  }
222
222
  break;
223
223
  case 'enter_filter_mode':
224
- stateManager.enterFilterMode();
224
+ stateManager.enterFilterMode(action.preserveQuery);
225
225
  break;
226
226
  case 'exit_filter_mode':
227
- stateManager.exitFilterMode();
227
+ stateManager.exitFilterMode(action.clearQuery);
228
228
  break;
229
229
  case 'filter_input':
230
230
  stateManager.appendToFilterQuery(action.char);
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packageCache = exports.CacheManager = void 0;
4
+ const config_1 = require("../config");
5
+ const persistent_cache_1 = require("./persistent-cache");
6
+ /**
7
+ * Unified cache manager that handles both in-memory and persistent disk caching.
8
+ * Consolidates caching logic used across registry services.
9
+ */
10
+ class CacheManager {
11
+ constructor(ttl = config_1.CACHE_TTL) {
12
+ this.memoryCache = new Map();
13
+ this.ttl = ttl;
14
+ }
15
+ /**
16
+ * Get cached data for a key, checking memory first, then disk.
17
+ * Returns null if not found or expired.
18
+ */
19
+ get(key) {
20
+ // Check in-memory cache first (fastest)
21
+ const memoryCached = this.memoryCache.get(key);
22
+ if (memoryCached && Date.now() - memoryCached.timestamp < this.ttl) {
23
+ return memoryCached.data;
24
+ }
25
+ // Check persistent disk cache (survives restarts)
26
+ const diskCached = persistent_cache_1.persistentCache.get(key);
27
+ if (diskCached) {
28
+ // Populate in-memory cache for subsequent accesses
29
+ this.memoryCache.set(key, {
30
+ data: diskCached,
31
+ timestamp: Date.now(),
32
+ });
33
+ return diskCached;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Store data in both memory and disk cache.
39
+ */
40
+ set(key, data) {
41
+ // Cache in memory
42
+ this.memoryCache.set(key, {
43
+ data,
44
+ timestamp: Date.now(),
45
+ });
46
+ // Cache to disk for persistence
47
+ persistent_cache_1.persistentCache.set(key, data);
48
+ }
49
+ /**
50
+ * Get data from cache or fetch it using the provided fetcher function.
51
+ * This is the main entry point for cache-aside pattern.
52
+ */
53
+ async getOrFetch(key, fetcher) {
54
+ // Try cache first
55
+ const cached = this.get(key);
56
+ if (cached) {
57
+ return cached;
58
+ }
59
+ // Fetch fresh data
60
+ const data = await fetcher();
61
+ if (data) {
62
+ this.set(key, data);
63
+ }
64
+ return data;
65
+ }
66
+ /**
67
+ * Check if a key exists and is not expired in cache.
68
+ */
69
+ has(key) {
70
+ return this.get(key) !== null;
71
+ }
72
+ /**
73
+ * Clear in-memory cache (useful for testing).
74
+ */
75
+ clear() {
76
+ this.memoryCache.clear();
77
+ }
78
+ /**
79
+ * Flush pending disk cache writes.
80
+ */
81
+ flush() {
82
+ persistent_cache_1.persistentCache.flush();
83
+ }
84
+ /**
85
+ * Get cache statistics.
86
+ */
87
+ getStats() {
88
+ return {
89
+ memoryEntries: this.memoryCache.size,
90
+ diskStats: persistent_cache_1.persistentCache.getStats(),
91
+ };
92
+ }
93
+ }
94
+ exports.CacheManager = CacheManager;
95
+ // Default package version cache instance
96
+ exports.packageCache = new CacheManager();
97
+ //# sourceMappingURL=cache-manager.js.map
@@ -21,4 +21,6 @@ __exportStar(require("./npm-registry"), exports);
21
21
  __exportStar(require("./jsdelivr-registry"), exports);
22
22
  __exportStar(require("./changelog-fetcher"), exports);
23
23
  __exportStar(require("./version-checker"), exports);
24
+ __exportStar(require("./persistent-cache"), exports);
25
+ __exportStar(require("./cache-manager"), exports);
24
26
  //# sourceMappingURL=index.js.map
@@ -40,6 +40,8 @@ const undici_1 = require("undici");
40
40
  const semver = __importStar(require("semver"));
41
41
  const config_1 = require("../config");
42
42
  const npm_registry_1 = require("./npm-registry");
43
+ const cache_manager_1 = require("./cache-manager");
44
+ const utils_1 = require("../ui/utils");
43
45
  // Create a persistent connection pool for jsDelivr CDN with optimal settings
44
46
  // This enables connection reuse and HTTP/1.1 keep-alive for blazing fast requests
45
47
  const jsdelivrPool = new undici_1.Pool('https://cdn.jsdelivr.net', {
@@ -52,7 +54,6 @@ const jsdelivrPool = new undici_1.Pool('https://cdn.jsdelivr.net', {
52
54
  // Batch configuration for progressive loading
53
55
  const BATCH_SIZE = 5;
54
56
  const BATCH_TIMEOUT_MS = 500;
55
- const packageCache = new Map();
56
57
  /**
57
58
  * Fetches package.json from jsdelivr CDN for a specific version tag using undici pool.
58
59
  * Uses connection pooling and keep-alive for maximum performance.
@@ -135,15 +136,15 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
135
136
  // Process individual package fetch with immediate npm fallback on failure
136
137
  const fetchPackageWithFallback = async (packageName) => {
137
138
  const currentVersion = currentVersions?.get(packageName);
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);
139
+ // Use CacheManager for unified caching (memory + disk)
140
+ const cached = cache_manager_1.packageCache.get(packageName);
141
+ if (cached) {
142
+ packageData.set(packageName, cached);
142
143
  completedCount++;
143
144
  if (onProgress) {
144
145
  onProgress(packageName, completedCount, total);
145
146
  }
146
- addToBatch(packageName, cached.data);
147
+ addToBatch(packageName, cached);
147
148
  return;
148
149
  }
149
150
  try {
@@ -168,10 +169,8 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
168
169
  const result = npmData.get(packageName);
169
170
  if (result) {
170
171
  packageData.set(packageName, result);
171
- packageCache.set(packageName, {
172
- data: result,
173
- timestamp: Date.now(),
174
- });
172
+ // CacheManager handles both memory and disk caching
173
+ cache_manager_1.packageCache.set(packageName, result);
175
174
  addToBatch(packageName, result);
176
175
  }
177
176
  completedCount++;
@@ -190,11 +189,8 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
190
189
  latestVersion,
191
190
  allVersions: allVersions.sort(semver.rcompare),
192
191
  };
193
- // Cache the result
194
- packageCache.set(packageName, {
195
- data: result,
196
- timestamp: Date.now(),
197
- });
192
+ // Cache the result using CacheManager (handles both memory and disk)
193
+ cache_manager_1.packageCache.set(packageName, result);
198
194
  packageData.set(packageName, result);
199
195
  completedCount++;
200
196
  if (onProgress) {
@@ -209,10 +205,8 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
209
205
  const result = npmData.get(packageName);
210
206
  if (result) {
211
207
  packageData.set(packageName, result);
212
- packageCache.set(packageName, {
213
- data: result,
214
- timestamp: Date.now(),
215
- });
208
+ // CacheManager handles both memory and disk caching
209
+ cache_manager_1.packageCache.set(packageName, result);
216
210
  addToBatch(packageName, result);
217
211
  }
218
212
  }
@@ -229,9 +223,11 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
229
223
  await Promise.all(packageNames.map(fetchPackageWithFallback));
230
224
  // Flush any remaining batch items
231
225
  flushBatch();
232
- // Clear the progress line and show completion time if no custom progress handler
226
+ // Flush persistent cache to disk
227
+ cache_manager_1.packageCache.flush();
228
+ // Clear the progress line if no custom progress handler
233
229
  if (!onProgress) {
234
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
230
+ utils_1.ConsoleUtils.clearProgress();
235
231
  }
236
232
  return packageData;
237
233
  }
@@ -239,7 +235,7 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
239
235
  * Clear the package cache (useful for testing)
240
236
  */
241
237
  function clearJsdelivrPackageCache() {
242
- packageCache.clear();
238
+ cache_manager_1.packageCache.clear();
243
239
  }
244
240
  /**
245
241
  * Close the jsDelivr connection pool (useful for graceful shutdown)
@@ -37,16 +37,17 @@ exports.getAllPackageData = getAllPackageData;
37
37
  exports.clearPackageCache = clearPackageCache;
38
38
  const semver = __importStar(require("semver"));
39
39
  const config_1 = require("../config");
40
- const packageCache = new Map();
40
+ const cache_manager_1 = require("./cache-manager");
41
+ const utils_1 = require("../ui/utils");
41
42
  /**
42
- * Fetches package data from npm registry with caching using native fetch.
43
- * Includes timeout support for slow connections.
43
+ * Fetches package data from npm registry.
44
+ * Uses the shared CacheManager for caching.
44
45
  */
45
46
  async function fetchPackageFromRegistry(packageName) {
46
- // Check cache first
47
- const cached = packageCache.get(packageName);
48
- if (cached && Date.now() - cached.timestamp < config_1.CACHE_TTL) {
49
- return cached.data;
47
+ // Use CacheManager for unified caching (memory + disk)
48
+ const cached = cache_manager_1.packageCache.get(packageName);
49
+ if (cached) {
50
+ return cached;
50
51
  }
51
52
  try {
52
53
  const url = `${config_1.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`;
@@ -78,11 +79,8 @@ async function fetchPackageFromRegistry(packageName) {
78
79
  latestVersion,
79
80
  allVersions,
80
81
  };
81
- // Cache the result
82
- packageCache.set(packageName, {
83
- data: result,
84
- timestamp: Date.now(),
85
- });
82
+ // Cache the result using CacheManager (handles both memory and disk)
83
+ cache_manager_1.packageCache.set(packageName, result);
86
84
  return result;
87
85
  }
88
86
  finally {
@@ -118,9 +116,11 @@ async function getAllPackageData(packageNames, onProgress) {
118
116
  });
119
117
  // Wait for all requests to complete
120
118
  await Promise.all(allPromises);
121
- // Clear the progress line and show completion time if no custom progress handler
119
+ // Flush persistent cache to disk
120
+ cache_manager_1.packageCache.flush();
121
+ // Clear the progress line if no custom progress handler
122
122
  if (!onProgress) {
123
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
123
+ utils_1.ConsoleUtils.clearProgress();
124
124
  }
125
125
  return packageData;
126
126
  }
@@ -128,6 +128,6 @@ async function getAllPackageData(packageNames, onProgress) {
128
128
  * Clear the package cache (useful for testing)
129
129
  */
130
130
  function clearPackageCache() {
131
- packageCache.clear();
131
+ cache_manager_1.packageCache.clear();
132
132
  }
133
133
  //# sourceMappingURL=npm-registry.js.map
@@ -0,0 +1,242 @@
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.persistentCache = void 0;
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const env_paths_1 = __importDefault(require("env-paths"));
10
+ // Cache TTL: 24 hours for disk cache (much longer than in-memory 5 minutes)
11
+ const DISK_CACHE_TTL = 24 * 60 * 60 * 1000;
12
+ // Maximum cache size (number of packages)
13
+ const MAX_CACHE_ENTRIES = 5000;
14
+ // Cache file format version (increment when structure changes)
15
+ const CACHE_VERSION = 1;
16
+ /**
17
+ * Persistent cache manager for package registry data.
18
+ * Stores cache on disk for fast repeated runs across CLI invocations.
19
+ */
20
+ class PersistentCacheManager {
21
+ constructor() {
22
+ this.index = null;
23
+ this.dirty = false;
24
+ const paths = (0, env_paths_1.default)('inup');
25
+ this.cacheDir = (0, path_1.join)(paths.cache, 'registry');
26
+ this.indexPath = (0, path_1.join)(this.cacheDir, 'index.json');
27
+ }
28
+ /**
29
+ * Ensure cache directory exists
30
+ */
31
+ ensureCacheDir() {
32
+ if (!(0, fs_1.existsSync)(this.cacheDir)) {
33
+ (0, fs_1.mkdirSync)(this.cacheDir, { recursive: true });
34
+ }
35
+ }
36
+ /**
37
+ * Load cache index from disk
38
+ */
39
+ loadIndex() {
40
+ if (this.index) {
41
+ return this.index;
42
+ }
43
+ try {
44
+ if ((0, fs_1.existsSync)(this.indexPath)) {
45
+ const content = (0, fs_1.readFileSync)(this.indexPath, 'utf-8');
46
+ const parsed = JSON.parse(content);
47
+ // Check cache version - invalidate if outdated
48
+ if (parsed.version !== CACHE_VERSION) {
49
+ this.clearCache();
50
+ this.index = { version: CACHE_VERSION, entries: {} };
51
+ return this.index;
52
+ }
53
+ this.index = parsed;
54
+ return this.index;
55
+ }
56
+ }
57
+ catch {
58
+ // Corrupted index, start fresh
59
+ }
60
+ this.index = { version: CACHE_VERSION, entries: {} };
61
+ return this.index;
62
+ }
63
+ /**
64
+ * Save cache index to disk
65
+ */
66
+ saveIndex() {
67
+ if (!this.dirty || !this.index) {
68
+ return;
69
+ }
70
+ try {
71
+ this.ensureCacheDir();
72
+ (0, fs_1.writeFileSync)(this.indexPath, JSON.stringify(this.index), 'utf-8');
73
+ this.dirty = false;
74
+ }
75
+ catch {
76
+ // Silently fail - cache is not critical
77
+ }
78
+ }
79
+ /**
80
+ * Generate a safe filename for a package name
81
+ */
82
+ getFilename(packageName) {
83
+ // Handle scoped packages: @scope/name -> scope__name
84
+ const safeName = packageName.replace(/^@/, '').replace(/\//g, '__');
85
+ return `${safeName}.json`;
86
+ }
87
+ /**
88
+ * Get cached data for a package
89
+ */
90
+ get(packageName) {
91
+ const index = this.loadIndex();
92
+ const entry = index.entries[packageName];
93
+ if (!entry) {
94
+ return null;
95
+ }
96
+ // Check TTL
97
+ if (Date.now() - entry.timestamp > DISK_CACHE_TTL) {
98
+ // Expired, remove from index
99
+ delete index.entries[packageName];
100
+ this.dirty = true;
101
+ return null;
102
+ }
103
+ // Read the actual cache file
104
+ try {
105
+ const filePath = (0, path_1.join)(this.cacheDir, entry.file);
106
+ if (!(0, fs_1.existsSync)(filePath)) {
107
+ delete index.entries[packageName];
108
+ this.dirty = true;
109
+ return null;
110
+ }
111
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
112
+ const cached = JSON.parse(content);
113
+ return {
114
+ latestVersion: cached.latestVersion,
115
+ allVersions: cached.allVersions,
116
+ };
117
+ }
118
+ catch {
119
+ // Corrupted cache file, remove from index
120
+ delete index.entries[packageName];
121
+ this.dirty = true;
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * Store data for a package
127
+ */
128
+ set(packageName, data) {
129
+ const index = this.loadIndex();
130
+ // Evict old entries if cache is too large
131
+ const entryCount = Object.keys(index.entries).length;
132
+ if (entryCount >= MAX_CACHE_ENTRIES) {
133
+ this.evictOldest(Math.floor(MAX_CACHE_ENTRIES * 0.1)); // Evict 10%
134
+ }
135
+ const filename = this.getFilename(packageName);
136
+ const entry = {
137
+ ...data,
138
+ timestamp: Date.now(),
139
+ };
140
+ try {
141
+ this.ensureCacheDir();
142
+ const filePath = (0, path_1.join)(this.cacheDir, filename);
143
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(entry), 'utf-8');
144
+ index.entries[packageName] = {
145
+ file: filename,
146
+ timestamp: Date.now(),
147
+ };
148
+ this.dirty = true;
149
+ }
150
+ catch {
151
+ // Silently fail - cache is not critical
152
+ }
153
+ }
154
+ /**
155
+ * Batch get multiple packages (returns map of found entries)
156
+ */
157
+ getMany(packageNames) {
158
+ const results = new Map();
159
+ for (const name of packageNames) {
160
+ const cached = this.get(name);
161
+ if (cached) {
162
+ results.set(name, cached);
163
+ }
164
+ }
165
+ return results;
166
+ }
167
+ /**
168
+ * Batch set multiple packages
169
+ */
170
+ setMany(entries) {
171
+ for (const [name, data] of entries) {
172
+ this.set(name, data);
173
+ }
174
+ this.flush();
175
+ }
176
+ /**
177
+ * Evict oldest cache entries
178
+ */
179
+ evictOldest(count) {
180
+ const index = this.loadIndex();
181
+ const entries = Object.entries(index.entries);
182
+ // Sort by timestamp (oldest first)
183
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
184
+ // Remove oldest entries
185
+ const toRemove = entries.slice(0, count);
186
+ for (const [packageName, entry] of toRemove) {
187
+ try {
188
+ const filePath = (0, path_1.join)(this.cacheDir, entry.file);
189
+ if ((0, fs_1.existsSync)(filePath)) {
190
+ (0, fs_1.unlinkSync)(filePath);
191
+ }
192
+ }
193
+ catch {
194
+ // Ignore deletion errors
195
+ }
196
+ delete index.entries[packageName];
197
+ }
198
+ this.dirty = true;
199
+ }
200
+ /**
201
+ * Clear all cache
202
+ */
203
+ clearCache() {
204
+ try {
205
+ if ((0, fs_1.existsSync)(this.cacheDir)) {
206
+ const files = (0, fs_1.readdirSync)(this.cacheDir);
207
+ for (const file of files) {
208
+ try {
209
+ (0, fs_1.unlinkSync)((0, path_1.join)(this.cacheDir, file));
210
+ }
211
+ catch {
212
+ // Ignore
213
+ }
214
+ }
215
+ }
216
+ }
217
+ catch {
218
+ // Ignore
219
+ }
220
+ this.index = { version: CACHE_VERSION, entries: {} };
221
+ this.dirty = true;
222
+ }
223
+ /**
224
+ * Flush pending changes to disk
225
+ */
226
+ flush() {
227
+ this.saveIndex();
228
+ }
229
+ /**
230
+ * Get cache statistics
231
+ */
232
+ getStats() {
233
+ const index = this.loadIndex();
234
+ return {
235
+ entries: Object.keys(index.entries).length,
236
+ cacheDir: this.cacheDir,
237
+ };
238
+ }
239
+ }
240
+ // Export singleton instance
241
+ exports.persistentCache = new PersistentCacheManager();
242
+ //# sourceMappingURL=persistent-cache.js.map
@@ -47,24 +47,34 @@ class InputHandler {
47
47
  }
48
48
  return;
49
49
  }
50
- // Check for '/' character to enter filter mode (only in normal mode, not in modal or already in filter)
51
- if (str === '/' && !uiState.showInfoModal && !uiState.filterMode) {
52
- this.onAction({ type: 'enter_filter_mode' });
50
+ // Check for '/' character to handle filter mode (only when not in modal)
51
+ if (str === '/' && !uiState.showInfoModal) {
52
+ if (uiState.filterMode) {
53
+ // Apply search (exit filter mode but keep the filter)
54
+ this.onAction({ type: 'exit_filter_mode' });
55
+ }
56
+ else {
57
+ // Enter filter mode - preserve query if one exists (to edit it)
58
+ this.onAction({ type: 'enter_filter_mode', preserveQuery: !!uiState.filterQuery });
59
+ }
53
60
  return;
54
61
  }
55
62
  // Handle filter mode input
56
63
  if (uiState.filterMode) {
64
+ // Check for escape key (either via key.name or raw escape character)
65
+ if ((key && key.name === 'escape') || str === '\x1b') {
66
+ // Escape clears the filter and exits filter mode
67
+ this.onAction({ type: 'exit_filter_mode', clearQuery: true });
68
+ return;
69
+ }
57
70
  if (key) {
58
71
  switch (key.name) {
59
- case 'escape':
60
- this.onAction({ type: 'exit_filter_mode' });
61
- return;
62
72
  case 'backspace':
63
73
  case 'delete':
64
74
  this.onAction({ type: 'filter_backspace' });
65
75
  return;
66
76
  case 'return':
67
- // Exit filter mode but keep the filter applied
77
+ // Apply search (exit filter mode but keep the filter)
68
78
  this.onAction({ type: 'exit_filter_mode' });
69
79
  return;
70
80
  case 'up':
@@ -116,10 +126,7 @@ class InputHandler {
116
126
  // Check if any packages are selected
117
127
  const selectedCount = states.filter((s) => s.selectedOption !== 'none').length;
118
128
  if (selectedCount === 0) {
119
- // Show warning and stay in selection mode
120
- console.log('\n' +
121
- '\x1b[33m⚠️ No packages selected. Press ↑/↓ to navigate and ←/→ to select versions, or ESC to exit.\x1b[39m');
122
- // Re-render will happen automatically
129
+ // Do nothing if no packages selected
123
130
  return;
124
131
  }
125
132
  this.cleanup();
@@ -164,13 +171,15 @@ class InputHandler {
164
171
  this.onAction({ type: 'toggle_theme_modal' });
165
172
  break;
166
173
  case 'escape':
167
- // Check if modal is open - if so, close it; otherwise cancel
174
+ // Close modal if open
168
175
  if (uiState.showInfoModal) {
169
176
  this.onAction({ type: 'toggle_info_modal' });
170
177
  }
171
- else {
172
- this.onAction({ type: 'cancel' });
178
+ else if (uiState.filterQuery) {
179
+ // Clear filter if one is applied
180
+ this.onAction({ type: 'exit_filter_mode', clearQuery: true });
173
181
  }
182
+ // Otherwise do nothing - Escape no longer exits the CLI
174
183
  break;
175
184
  }
176
185
  }
@@ -178,11 +187,7 @@ class InputHandler {
178
187
  this.onAction({ type: 'resize', height });
179
188
  }
180
189
  cleanup() {
181
- utils_1.CursorUtils.show();
182
- if (process.stdin.setRawMode) {
183
- process.stdin.setRawMode(false);
184
- }
185
- process.stdin.pause();
190
+ utils_1.CursorUtils.cleanup();
186
191
  }
187
192
  }
188
193
  exports.InputHandler = InputHandler;
@@ -219,11 +224,7 @@ class ConfirmationInputHandler {
219
224
  }
220
225
  }
221
226
  cleanup() {
222
- utils_1.CursorUtils.show();
223
- if (process.stdin.setRawMode) {
224
- process.stdin.setRawMode(false);
225
- }
226
- process.stdin.pause();
227
+ utils_1.CursorUtils.cleanup();
227
228
  }
228
229
  }
229
230
  exports.ConfirmationInputHandler = ConfirmationInputHandler;
@@ -7,12 +7,9 @@ exports.renderPackageInfoLoading = renderPackageInfoLoading;
7
7
  exports.renderPackageInfoModal = renderPackageInfoModal;
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const themes_colors_1 = require("../themes-colors");
10
- /**
11
- * Remove ANSI color codes from a string for length calculation
12
- */
13
- function stripAnsi(str) {
14
- return str.replace(/\u001b\[[0-9;]*m/g, '');
15
- }
10
+ const utils_1 = require("../utils");
11
+ // Use shared ANSI stripping utility
12
+ const stripAnsi = utils_1.VersionUtils.stripAnsi;
16
13
  /**
17
14
  * Format a number for display (e.g., 1000000 -> "1M", 1000 -> "1K")
18
15
  */
@@ -213,6 +213,12 @@ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forc
213
213
  const padding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(filterDisplay));
214
214
  output.push(filterDisplay + ' '.repeat(padding));
215
215
  }
216
+ else if (filterQuery) {
217
+ // Show applied filter when not in filter mode but filter is active
218
+ const filterDisplay = ' ' + chalk_1.default.bold.white('Search: ') + (0, themes_colors_1.getThemeColor)('primary')(filterQuery) + (0, themes_colors_1.getThemeColor)('textSecondary')(' (press / to edit)');
219
+ const padding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(filterDisplay));
220
+ output.push(filterDisplay + ' '.repeat(padding));
221
+ }
216
222
  else {
217
223
  // Show instructions when not filtering
218
224
  output.push(' ' +
@@ -249,51 +255,54 @@ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forc
249
255
  const endItem = Math.min(scrollOffset + maxVisibleItems, totalVisualItems);
250
256
  let statusLine = '';
251
257
  if (filterMode) {
252
- // In filter mode, show ESC to exit filter
258
+ // In filter mode, show Enter to apply and ESC to clear
253
259
  if (totalPackages === 0) {
254
260
  statusLine = (0, themes_colors_1.getThemeColor)('warning')(`No matches found`) +
255
261
  ' ' +
256
- (0, themes_colors_1.getThemeColor)('textSecondary')('Esc ') +
257
- (0, themes_colors_1.getThemeColor)('textSecondary')('Clear filter');
262
+ chalk_1.default.bold.white('Esc ') + chalk_1.default.gray('Clear');
258
263
  }
259
264
  else if (totalVisualItems > maxVisibleItems) {
260
- statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing ${chalk_1.default.white(startItem)}-${chalk_1.default.white(endItem)} of ${chalk_1.default.white(totalPackages)} matches (${chalk_1.default.white(totalBeforeFilter)} total)`) +
265
+ statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing ${chalk_1.default.white(startItem)}-${chalk_1.default.white(endItem)} of ${chalk_1.default.white(totalPackages)} matches`) +
261
266
  ' ' +
262
- (0, themes_colors_1.getThemeColor)('textSecondary')('Esc ') +
263
- (0, themes_colors_1.getThemeColor)('textSecondary')('Clear filter');
267
+ chalk_1.default.bold.white('Enter ') + chalk_1.default.gray('Apply') +
268
+ ' ' +
269
+ chalk_1.default.bold.white('Esc ') + chalk_1.default.gray('Clear');
264
270
  }
265
271
  else {
266
- statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing all ${chalk_1.default.white(totalPackages)} matches (${chalk_1.default.white(totalBeforeFilter)} total)`) +
272
+ statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing all ${chalk_1.default.white(totalPackages)} matches`) +
273
+ ' ' +
274
+ chalk_1.default.bold.white('Enter ') + chalk_1.default.gray('Apply') +
267
275
  ' ' +
268
- (0, themes_colors_1.getThemeColor)('textSecondary')('Esc ') +
269
- (0, themes_colors_1.getThemeColor)('textSecondary')('Clear filter');
276
+ chalk_1.default.bold.white('Esc ') + chalk_1.default.gray('Clear');
270
277
  }
271
278
  }
272
279
  else if (totalPackages < totalBeforeFilter) {
273
280
  // Filter is applied but not in filter mode
274
281
  if (totalVisualItems > maxVisibleItems) {
275
- statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing ${chalk_1.default.white(startItem)}-${chalk_1.default.white(endItem)} of ${chalk_1.default.white(totalPackages)} matches (${chalk_1.default.white(totalBeforeFilter)} total)`) +
282
+ statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing ${chalk_1.default.white(startItem)}-${chalk_1.default.white(endItem)} of ${chalk_1.default.white(totalPackages)} matches`) +
283
+ ' ' +
284
+ chalk_1.default.bold.white('D/P/O ') + chalk_1.default.gray('Filter') +
276
285
  ' ' +
277
- (0, themes_colors_1.getThemeColor)('textSecondary')('/ ') +
278
- (0, themes_colors_1.getThemeColor)('textSecondary')('Edit filter') +
286
+ chalk_1.default.bold.white('M ') + chalk_1.default.gray('Minor') +
279
287
  ' ' +
280
- (0, themes_colors_1.getThemeColor)('textSecondary')('Enter ') +
281
- (0, themes_colors_1.getThemeColor)('textSecondary')('Confirm') +
288
+ chalk_1.default.bold.white('L ') + chalk_1.default.gray('All') +
282
289
  ' ' +
283
- (0, themes_colors_1.getThemeColor)('textSecondary')('Esc ') +
284
- (0, themes_colors_1.getThemeColor)('textSecondary')('Cancel');
290
+ chalk_1.default.bold.white('U ') + chalk_1.default.gray('None') +
291
+ ' ' +
292
+ chalk_1.default.bold.white('Esc ') + chalk_1.default.gray('Clear');
285
293
  }
286
294
  else {
287
- statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing all ${chalk_1.default.white(totalPackages)} matches (${chalk_1.default.white(totalBeforeFilter)} total)`) +
295
+ statusLine = (0, themes_colors_1.getThemeColor)('textSecondary')(`Showing all ${chalk_1.default.white(totalPackages)} matches`) +
296
+ ' ' +
297
+ chalk_1.default.bold.white('D/P/O ') + chalk_1.default.gray('Filter') +
298
+ ' ' +
299
+ chalk_1.default.bold.white('M ') + chalk_1.default.gray('Minor') +
288
300
  ' ' +
289
- (0, themes_colors_1.getThemeColor)('textSecondary')('/ ') +
290
- chalk_1.default.gray('Edit filter') +
301
+ chalk_1.default.bold.white('L ') + chalk_1.default.gray('All') +
291
302
  ' ' +
292
- chalk_1.default.gray('Enter ') +
293
- chalk_1.default.gray('Confirm') +
303
+ chalk_1.default.bold.white('U ') + chalk_1.default.gray('None') +
294
304
  ' ' +
295
- chalk_1.default.gray('Esc ') +
296
- chalk_1.default.gray('Cancel');
305
+ chalk_1.default.bold.white('Esc ') + chalk_1.default.gray('Clear');
297
306
  }
298
307
  }
299
308
  else {
@@ -301,23 +310,18 @@ function renderInterface(states, currentRow, scrollOffset, maxVisibleItems, forc
301
310
  if (totalVisualItems > maxVisibleItems) {
302
311
  statusLine = chalk_1.default.gray(`Showing ${chalk_1.default.white(startItem)}-${chalk_1.default.white(endItem)} of ${chalk_1.default.white(totalPackages)} packages`) +
303
312
  ' ' +
304
- chalk_1.default.gray('Enter ') +
305
- chalk_1.default.gray('Confirm') +
306
- ' ' +
307
- chalk_1.default.gray('Esc ') +
308
- chalk_1.default.gray('Cancel');
313
+ chalk_1.default.bold.white('Enter ') + chalk_1.default.gray('Confirm');
309
314
  }
310
315
  else {
311
316
  statusLine = chalk_1.default.gray(`Showing all ${chalk_1.default.white(totalPackages)} packages`) +
312
317
  ' ' +
313
- chalk_1.default.gray('Enter ') +
314
- chalk_1.default.gray('Confirm') +
315
- ' ' +
316
- chalk_1.default.gray('Esc ') +
317
- chalk_1.default.gray('Cancel');
318
+ chalk_1.default.bold.white('Enter ') + chalk_1.default.gray('Confirm');
318
319
  }
319
320
  }
320
- output.push(' ' + statusLine);
321
+ // Pad status line to terminal width to clear any leftover characters
322
+ const statusLineFull = ' ' + statusLine;
323
+ const statusPadding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(statusLineFull));
324
+ output.push(statusLineFull + ' '.repeat(statusPadding));
321
325
  output.push('');
322
326
  // Render visible items
323
327
  if (renderableItems && renderableItems.length > 0) {
@@ -21,13 +21,17 @@ class FilterManager {
21
21
  getFilterQuery() {
22
22
  return this.state.filterQuery;
23
23
  }
24
- enterFilterMode() {
24
+ enterFilterMode(preserveQuery = false) {
25
25
  this.state.filterMode = true;
26
- this.state.filterQuery = '';
26
+ if (!preserveQuery) {
27
+ this.state.filterQuery = '';
28
+ }
27
29
  }
28
- exitFilterMode() {
30
+ exitFilterMode(clearQuery = false) {
29
31
  this.state.filterMode = false;
30
- this.state.filterQuery = '';
32
+ if (clearQuery) {
33
+ this.state.filterQuery = '';
34
+ }
31
35
  }
32
36
  updateFilterQuery(query) {
33
37
  this.state.filterQuery = query;
@@ -161,12 +161,12 @@ class StateManager {
161
161
  this.renderState.forceFullRender = true;
162
162
  }
163
163
  // Filter delegation
164
- enterFilterMode() {
165
- this.filterManager.enterFilterMode();
164
+ enterFilterMode(preserveQuery = false) {
165
+ this.filterManager.enterFilterMode(preserveQuery);
166
166
  // Use incremental render for search mode toggle (no blink)
167
167
  }
168
- exitFilterMode() {
169
- this.filterManager.exitFilterMode();
168
+ exitFilterMode(clearQuery = false) {
169
+ this.filterManager.exitFilterMode(clearQuery);
170
170
  this.navigationManager.setCurrentRow(0);
171
171
  this.navigationManager.setScrollOffset(0);
172
172
  // Use incremental render for search mode toggle (no blink)
@@ -89,6 +89,39 @@ const themeColorDefinitions = {
89
89
  text: '#ABB2BF',
90
90
  textSecondary: '#5C6370',
91
91
  },
92
+ gruvbox: {
93
+ bg: '#282828',
94
+ primary: '#83A598',
95
+ secondary: '#D3869B',
96
+ success: '#B8BB26',
97
+ warning: '#FABD2F',
98
+ error: '#FB4934',
99
+ border: '#504945',
100
+ text: '#EBDBB2',
101
+ textSecondary: '#A89984',
102
+ },
103
+ solarized: {
104
+ bg: '#002B36',
105
+ primary: '#268BD2',
106
+ secondary: '#D33682',
107
+ success: '#859900',
108
+ warning: '#B58900',
109
+ error: '#DC322F',
110
+ border: '#073642',
111
+ text: '#839496',
112
+ textSecondary: '#657B83',
113
+ },
114
+ github: {
115
+ bg: '#0D1117',
116
+ primary: '#58A6FF',
117
+ secondary: '#BC8CFF',
118
+ success: '#3FB950',
119
+ warning: '#D29922',
120
+ error: '#F85149',
121
+ border: '#30363D',
122
+ text: '#C9D1D9',
123
+ textSecondary: '#8B949E',
124
+ },
92
125
  };
93
126
  // Helper to apply color - handles both hex and named colors
94
127
  function applyColor(color, text) {
package/dist/ui/themes.js CHANGED
@@ -13,6 +13,9 @@ const themesMetadata = {
13
13
  monokai: 'Monokai',
14
14
  tokyonight: 'Tokyo Night',
15
15
  onedark: 'One Dark',
16
+ gruvbox: 'Gruvbox',
17
+ solarized: 'Solarized',
18
+ github: 'GitHub',
16
19
  };
17
20
  // Theme definitions
18
21
  exports.themes = Object.entries(themesMetadata).reduce((acc, [key, name]) => {
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * Cursor utility functions for terminal control
3
+ * Cursor and terminal utility functions
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CursorUtils = void 0;
6
+ exports.ConsoleUtils = exports.CursorUtils = void 0;
7
7
  exports.CursorUtils = {
8
8
  /**
9
9
  * Hide the cursor in the terminal
@@ -29,5 +29,37 @@ exports.CursorUtils = {
29
29
  clearToEndOfScreen() {
30
30
  process.stdout.write('\x1b[J');
31
31
  },
32
+ /**
33
+ * Clean up terminal state - restore cursor and disable raw mode.
34
+ * Used when exiting interactive mode.
35
+ */
36
+ cleanup() {
37
+ exports.CursorUtils.show();
38
+ if (process.stdin.setRawMode) {
39
+ process.stdin.setRawMode(false);
40
+ }
41
+ process.stdin.pause();
42
+ },
43
+ };
44
+ /**
45
+ * Console utilities for progress display and line clearing
46
+ */
47
+ exports.ConsoleUtils = {
48
+ /**
49
+ * Default line width for clearing progress messages
50
+ */
51
+ LINE_WIDTH: 80,
52
+ /**
53
+ * Show a progress message on the current line (overwrites previous content)
54
+ */
55
+ showProgress(message) {
56
+ process.stdout.write(`\r${' '.repeat(exports.ConsoleUtils.LINE_WIDTH)}\r${message}`);
57
+ },
58
+ /**
59
+ * Clear the current progress line
60
+ */
61
+ clearProgress() {
62
+ process.stdout.write('\r' + ' '.repeat(exports.ConsoleUtils.LINE_WIDTH) + '\r');
63
+ },
32
64
  };
33
65
  //# sourceMappingURL=cursor.js.map
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CursorUtils = exports.VersionUtils = void 0;
3
+ exports.ConsoleUtils = exports.CursorUtils = exports.VersionUtils = void 0;
4
4
  var version_1 = require("./version");
5
5
  Object.defineProperty(exports, "VersionUtils", { enumerable: true, get: function () { return version_1.VersionUtils; } });
6
6
  var cursor_1 = require("./cursor");
7
7
  Object.defineProperty(exports, "CursorUtils", { enumerable: true, get: function () { return cursor_1.CursorUtils; } });
8
+ Object.defineProperty(exports, "ConsoleUtils", { enumerable: true, get: function () { return cursor_1.ConsoleUtils; } });
8
9
  //# sourceMappingURL=index.js.map
@@ -5,6 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.VersionUtils = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
+ /**
9
+ * ANSI escape code pattern for stripping terminal colors
10
+ */
11
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
8
12
  class VersionUtils {
9
13
  static applyVersionPrefix(originalSpecifier, targetVersion) {
10
14
  // Extract prefix from original specifier (^ or ~)
@@ -13,9 +17,17 @@ class VersionUtils {
13
17
  // Return target version with same prefix
14
18
  return prefix + targetVersion;
15
19
  }
20
+ /**
21
+ * Strip ANSI escape codes from a string
22
+ */
23
+ static stripAnsi(str) {
24
+ return str.replace(ANSI_PATTERN, '');
25
+ }
26
+ /**
27
+ * Get the visual length of a string (excluding ANSI codes)
28
+ */
16
29
  static getVisualLength(str) {
17
- // Strip ANSI escape codes to get visual length
18
- return str.replace(/\u001b\[[0-9;]*m/g, '').length;
30
+ return VersionUtils.stripAnsi(str).length;
19
31
  }
20
32
  /**
21
33
  * Truncate text with ellipsis in the middle if it exceeds maxLength
@@ -35,7 +47,7 @@ class VersionUtils {
35
47
  const startLength = Math.ceil(availableLength / 2);
36
48
  const endLength = Math.floor(availableLength / 2);
37
49
  // Extract raw text without ANSI codes to calculate positions
38
- const rawText = str.replace(/\u001b\[[0-9;]*m/g, '');
50
+ const rawText = VersionUtils.stripAnsi(str);
39
51
  const start = rawText.substring(0, startLength);
40
52
  const end = rawText.substring(rawText.length - endLength);
41
53
  return start + ellipsis + end;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
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": {