inup 1.5.2 → 1.5.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.
@@ -45,8 +45,8 @@ class PackageDetector {
45
45
  constructor(options) {
46
46
  this.packageJsonPath = null;
47
47
  this.packageJson = null;
48
- this.batchSize = 25;
49
- this.batchConcurrency = 25;
48
+ this.batchSize = 10;
49
+ this.maxConcurrency = 10;
50
50
  this.cwd = options?.cwd || process.cwd();
51
51
  this.excludePatterns = options?.excludePatterns || [];
52
52
  this.ignorePackages = options?.ignorePackages || [];
@@ -95,45 +95,47 @@ class PackageDetector {
95
95
  let lastBatchEndAt = Date.now();
96
96
  const tFetch = Date.now();
97
97
  utils_3.debugLog.info('PackageDetector', 'fetching version data via npm registry in batches');
98
- await (0, services_1.getAllPackageDataBatched)(prepared.uniquePackages, (batch) => {
99
- const batchStart = lastBatchEndAt;
100
- let batchFailedCount = 0;
101
- const batchItems = batch.map((batchItem) => {
102
- const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
103
- packageLookup.set(batchItem.packageName, packageInfo);
104
- resolved++;
105
- const isFailed = batchItem.data.latestVersion === 'unknown';
106
- if (isFailed) {
107
- failed++;
108
- batchFailedCount++;
109
- performanceTracker.recordFailedPackage(batchItem.packageName);
110
- }
111
- return {
112
- packageName: batchItem.packageName,
113
- packageInfo,
114
- failed: isFailed,
115
- };
116
- });
117
- const batchEnd = Date.now();
118
- performanceTracker.recordBatch({
119
- index: batchIndex++,
120
- size: batch.length,
121
- durationMs: batchEnd - batchStart,
122
- failedCount: batchFailedCount,
123
- });
124
- lastBatchEndAt = batchEnd;
125
- performanceTracker.recordCounts({ resolved, failed });
126
- const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
127
- onEvent({
128
- type: 'batch',
129
- payload: {
130
- batch: batchItems,
131
- progress,
132
- },
133
- });
134
- }, prepared.currentVersions, {
98
+ await (0, services_1.fetchPackageVersions)(prepared.uniquePackages, {
99
+ currentVersions: prepared.currentVersions,
135
100
  batchSize: this.batchSize,
136
- concurrency: this.batchConcurrency,
101
+ maxConcurrency: this.maxConcurrency,
102
+ onBatchReady: (batch) => {
103
+ const batchStart = lastBatchEndAt;
104
+ let batchFailedCount = 0;
105
+ const batchItems = batch.map((batchItem) => {
106
+ const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
107
+ packageLookup.set(batchItem.packageName, packageInfo);
108
+ resolved++;
109
+ const isFailed = batchItem.data.latestVersion === 'unknown';
110
+ if (isFailed) {
111
+ failed++;
112
+ batchFailedCount++;
113
+ performanceTracker.recordFailedPackage(batchItem.packageName);
114
+ }
115
+ return {
116
+ packageName: batchItem.packageName,
117
+ packageInfo,
118
+ failed: isFailed,
119
+ };
120
+ });
121
+ const batchEnd = Date.now();
122
+ performanceTracker.recordBatch({
123
+ index: batchIndex++,
124
+ size: batch.length,
125
+ durationMs: batchEnd - batchStart,
126
+ failedCount: batchFailedCount,
127
+ });
128
+ lastBatchEndAt = batchEnd;
129
+ performanceTracker.recordCounts({ resolved, failed });
130
+ const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
131
+ onEvent({
132
+ type: 'batch',
133
+ payload: {
134
+ batch: batchItems,
135
+ progress,
136
+ },
137
+ });
138
+ },
137
139
  });
138
140
  utils_3.debugLog.perf('PackageDetector', `registry fetch (${resolved}/${prepared.uniquePackages.length} resolved)`, tFetch);
139
141
  performanceTracker.recordPhaseDuration('registryFetch', Date.now() - tFetch);
@@ -348,6 +348,11 @@ class InteractiveUI {
348
348
  return;
349
349
  }
350
350
  break;
351
+ case 'switch_info_modal_tab': {
352
+ const nextTab = stateManager.getInfoModalTab() === 'info' ? 'usedBy' : 'info';
353
+ stateManager.setInfoModalTab(nextTab);
354
+ break;
355
+ }
351
356
  case 'navigate_info_modal_version':
352
357
  {
353
358
  if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
@@ -553,15 +558,20 @@ class InteractiveUI {
553
558
  }
554
559
  else {
555
560
  // Show full info with scroll support
556
- const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset);
561
+ const activeTab = uiState.infoModalTab;
562
+ const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset, activeTab);
557
563
  infoModalMaxScrollOffset = result.maxScrollOffset;
558
564
  stateManager.clampInfoModalScrollOffset(infoModalMaxScrollOffset);
559
565
  const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
560
566
  ? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
561
567
  : '';
568
+ const versionHint = activeTab === 'info'
569
+ ? chalk_1.default.bold.white('←/→ ') + chalk_1.default.gray('Version · ')
570
+ : '';
562
571
  renderModalViewport('info-modal', scrollHint +
563
- chalk_1.default.bold.white('←/→ ') +
564
- chalk_1.default.gray('Version · ') +
572
+ versionHint +
573
+ chalk_1.default.bold.white('Tab ') +
574
+ chalk_1.default.gray('Switch tab · ') +
565
575
  chalk_1.default.bold.white('I / Esc ') +
566
576
  chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
567
577
  }
@@ -33,8 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.getAllPackageData = getAllPackageData;
37
- exports.getAllPackageDataBatched = getAllPackageDataBatched;
36
+ exports.fetchPackageVersions = fetchPackageVersions;
38
37
  exports.clearPackageCache = clearPackageCache;
39
38
  exports.closeRegistryPool = closeRegistryPool;
40
39
  const semver = __importStar(require("semver"));
@@ -46,19 +45,25 @@ const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
46
45
  const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
47
46
  const config_1 = require("../config");
48
47
  const jsdelivr_registry_1 = require("./jsdelivr-registry");
49
- const utils_1 = require("../ui/utils");
50
48
  const inFlightLookups = new Map();
51
49
  const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
52
50
  const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
51
+ // Few connections + many requests per connection = maximum keep-alive reuse.
52
+ // No per-request timeouts: correctness matters more than speed for a CLI that
53
+ // runs on demand. Slow responses are tolerated; only true errors cause retry.
53
54
  const registryPool = new undici_1.Pool(registryOrigin, {
54
- connections: 64,
55
- pipelining: 10,
55
+ connections: 6,
56
+ pipelining: 1,
56
57
  keepAliveTimeout: 30000,
57
58
  keepAliveMaxTimeout: 600000,
58
- headersTimeout: config_1.REQUEST_TIMEOUT,
59
- bodyTimeout: config_1.REQUEST_TIMEOUT,
59
+ headersTimeout: 0,
60
+ bodyTimeout: 0,
61
+ connectTimeout: 15000,
60
62
  allowH2: false,
61
63
  });
64
+ const MAX_REGISTRY_ATTEMPTS = 3;
65
+ const RETRY_BACKOFF_MS = [500, 1500, 3000];
66
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
62
67
  const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
63
68
  const isTransientNetworkError = (error) => {
64
69
  if (!(error instanceof Error)) {
@@ -104,12 +109,14 @@ function parseVersions(raw) {
104
109
  const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
105
110
  return { latestVersion, allVersions };
106
111
  }
107
- async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
112
+ const encodeRegistryPath = (packageName) => {
113
+ const encodedName = packageName.startsWith('@')
114
+ ? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
115
+ : encodeURIComponent(packageName);
116
+ return `${registryPathPrefix}/${encodedName}`;
117
+ };
118
+ async function attemptRegistryFetch(path) {
108
119
  try {
109
- const encodedName = packageName.startsWith('@')
110
- ? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
111
- : encodeURIComponent(packageName);
112
- const path = `${registryPathPrefix}/${encodedName}`;
113
120
  const { statusCode, headers, body } = await registryPool.request({
114
121
  path,
115
122
  method: 'GET',
@@ -117,16 +124,16 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
117
124
  accept: 'application/vnd.npm.install-v1+json',
118
125
  'accept-encoding': 'gzip, deflate, br',
119
126
  },
120
- headersTimeout: config_1.REQUEST_TIMEOUT,
121
- bodyTimeout: config_1.REQUEST_TIMEOUT,
127
+ headersTimeout: 0,
128
+ bodyTimeout: 0,
122
129
  blocking: false,
123
130
  });
124
131
  if (statusCode < 200 || statusCode >= 300) {
125
- await body.dump();
132
+ await body.dump().catch(() => undefined);
126
133
  if (isRetryableStatus(statusCode)) {
127
- return await fetchFromJsdelivrFallback(packageName, currentVersion);
134
+ return { kind: 'retryable' };
128
135
  }
129
- return { latestVersion: 'unknown', allVersions: [] };
136
+ return { kind: 'not-found' };
130
137
  }
131
138
  const raw = Buffer.from(await body.arrayBuffer());
132
139
  const encodingHeader = headers['content-encoding'];
@@ -146,97 +153,128 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
146
153
  else {
147
154
  decoded = raw;
148
155
  }
149
- return parseVersions(decoded.toString('utf8'));
156
+ return { kind: 'success', data: parseVersions(decoded.toString('utf8')) };
150
157
  }
151
158
  catch (error) {
152
159
  if (isTransientNetworkError(error)) {
153
- return await fetchFromJsdelivrFallback(packageName, currentVersion);
160
+ return { kind: 'transient' };
154
161
  }
155
- return { latestVersion: 'unknown', allVersions: [] };
162
+ // Unknown error: treat as transient so we try the fallback rather than
163
+ // silently returning 'unknown'.
164
+ return { kind: 'transient' };
156
165
  }
157
166
  }
158
- /**
159
- * Fetches package version data from npm registry for multiple packages.
160
- * Uses an undici Pool with HTTP/1.1 pipelining for high throughput.
161
- * Only returns valid semantic versions (X.Y.Z format, excluding pre-releases).
162
- */
163
- async function getAllPackageData(packageNames, onProgress, currentVersions) {
164
- const packageData = new Map();
165
- if (packageNames.length === 0) {
166
- return packageData;
167
- }
168
- const total = packageNames.length;
169
- let completedCount = 0;
170
- const allPromises = packageNames.map(async (packageName) => {
171
- const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
172
- packageData.set(packageName, data);
173
- completedCount++;
174
- if (onProgress) {
175
- onProgress(packageName, completedCount, total);
167
+ async function fetchFromRegistryWithRetries(path) {
168
+ let lastOutcome = { kind: 'transient' };
169
+ for (let attempt = 0; attempt < MAX_REGISTRY_ATTEMPTS; attempt++) {
170
+ const outcome = await attemptRegistryFetch(path);
171
+ if (outcome.kind === 'success' || outcome.kind === 'not-found') {
172
+ return outcome;
173
+ }
174
+ lastOutcome = outcome;
175
+ if (attempt < MAX_REGISTRY_ATTEMPTS - 1) {
176
+ const backoff = RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)];
177
+ await sleep(backoff);
176
178
  }
177
- });
178
- await Promise.all(allPromises);
179
- if (!onProgress) {
180
- utils_1.ConsoleUtils.clearProgress();
181
179
  }
182
- return packageData;
180
+ return lastOutcome;
183
181
  }
184
- async function runWithConcurrencyLimit(items, concurrency, worker) {
185
- if (items.length === 0) {
186
- return;
182
+ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
183
+ const path = encodeRegistryPath(packageName);
184
+ const outcome = await fetchFromRegistryWithRetries(path);
185
+ if (outcome.kind === 'success') {
186
+ return outcome.data;
187
187
  }
188
- const limit = Math.max(1, Math.min(concurrency, items.length));
189
- let nextIndex = 0;
190
- const runWorker = async () => {
191
- while (nextIndex < items.length) {
192
- const currentIndex = nextIndex++;
193
- await worker(items[currentIndex], currentIndex);
194
- }
195
- };
196
- await Promise.all(Array.from({ length: limit }, () => runWorker()));
188
+ if (outcome.kind === 'not-found') {
189
+ return { latestVersion: 'unknown', allVersions: [] };
190
+ }
191
+ // Only reach here after exhausted retries against real errors — try jsdelivr
192
+ // as last-resort safety net so we don't silently return 'unknown'.
193
+ const fallback = await fetchFromJsdelivrFallback(packageName, currentVersion).catch(() => null);
194
+ return fallback ?? { latestVersion: 'unknown', allVersions: [] };
197
195
  }
198
- async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersions, options = {}) {
196
+ /**
197
+ * Fetches version data for a list of packages from the npm registry.
198
+ *
199
+ * Concurrency model:
200
+ * - `maxConcurrency` is a global cap on in-flight fetches at any moment.
201
+ * It doesn't interact with batch size — batches exist only to group
202
+ * emissions for the UI.
203
+ * - No per-request timeouts: slow responses are allowed to finish. Real
204
+ * network errors are retried with exponential backoff; after that, we
205
+ * fall back to jsdelivr as a last resort. A result is never silently
206
+ * dropped due to slowness.
207
+ *
208
+ * Callbacks:
209
+ * - `onBatchReady` fires once a whole emission batch has resolved, in
210
+ * original batch order.
211
+ */
212
+ async function fetchPackageVersions(packageNames, options = {}) {
199
213
  const packageData = new Map();
200
214
  if (packageNames.length === 0) {
201
215
  return packageData;
202
216
  }
203
217
  const batchSizes = options.batchSizes && options.batchSizes.length > 0
204
218
  ? options.batchSizes.map((size) => Math.max(1, size))
205
- : [Math.max(1, options.batchSize ?? 20)];
206
- const concurrency = Math.max(1, options.concurrency ?? 5);
219
+ : [Math.max(1, options.batchSize ?? 25)];
220
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? 10);
207
221
  const total = packageNames.length;
208
222
  let completedCount = 0;
209
- let batchStart = 0;
210
- let batchIndex = 0;
211
- const batchPromises = [];
212
223
  const pendingEmissions = new Map();
213
224
  let nextEmitIndex = 0;
214
225
  const flushPending = () => {
215
226
  while (pendingEmissions.has(nextEmitIndex)) {
216
227
  const ready = pendingEmissions.get(nextEmitIndex);
217
228
  pendingEmissions.delete(nextEmitIndex);
218
- onBatchReady?.(ready);
229
+ options.onBatchReady?.(ready);
219
230
  nextEmitIndex++;
220
231
  }
221
232
  };
233
+ // Global semaphore: `maxConcurrency` is the total in-flight cap across
234
+ // all batches. Batches don't gate concurrency — only emission order.
235
+ let inFlight = 0;
236
+ const waiters = [];
237
+ const acquire = async () => {
238
+ if (inFlight < maxConcurrency) {
239
+ inFlight++;
240
+ return;
241
+ }
242
+ await new Promise((resolve) => waiters.push(resolve));
243
+ inFlight++;
244
+ };
245
+ const release = () => {
246
+ inFlight--;
247
+ const next = waiters.shift();
248
+ if (next)
249
+ next();
250
+ };
251
+ const batchPromises = [];
252
+ let batchStart = 0;
253
+ let batchIndex = 0;
222
254
  while (batchStart < packageNames.length) {
223
255
  const batchSize = batchSizes[Math.min(batchIndex, batchSizes.length - 1)];
224
256
  const batchNames = packageNames.slice(batchStart, batchStart + batchSize);
225
257
  const capturedBatchIndex = batchIndex;
226
258
  const batchResults = new Array(batchNames.length);
227
- const batchPromise = runWithConcurrencyLimit(batchNames, concurrency, async (packageName, itemIndex) => {
228
- const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
229
- packageData.set(packageName, data);
230
- completedCount++;
231
- batchResults[itemIndex] = {
232
- packageName,
233
- data,
234
- completed: completedCount,
235
- total,
236
- batchIndex: capturedBatchIndex,
237
- itemIndex,
238
- };
239
- }).then(() => {
259
+ const batchPromise = Promise.all(batchNames.map(async (packageName, itemIndex) => {
260
+ await acquire();
261
+ try {
262
+ const data = await getFreshPackageData(packageName, options.currentVersions?.get(packageName));
263
+ packageData.set(packageName, data);
264
+ completedCount++;
265
+ batchResults[itemIndex] = {
266
+ packageName,
267
+ data,
268
+ completed: completedCount,
269
+ total,
270
+ batchIndex: capturedBatchIndex,
271
+ itemIndex,
272
+ };
273
+ }
274
+ finally {
275
+ release();
276
+ }
277
+ })).then(() => {
240
278
  pendingEmissions.set(capturedBatchIndex, batchResults.filter(Boolean));
241
279
  flushPending();
242
280
  });
@@ -81,6 +81,9 @@ class InputHandler {
81
81
  case 'I':
82
82
  this.onAction({ type: 'toggle_info_modal' });
83
83
  return;
84
+ case 'tab':
85
+ this.onAction({ type: 'switch_info_modal_tab' });
86
+ return;
84
87
  case 'up':
85
88
  this.onAction({ type: 'scroll_info_modal_up' });
86
89
  return;
@@ -88,10 +91,14 @@ class InputHandler {
88
91
  this.onAction({ type: 'scroll_info_modal_down' });
89
92
  return;
90
93
  case 'left':
91
- this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
94
+ if (uiState.infoModalTab === 'info') {
95
+ this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
96
+ }
92
97
  return;
93
98
  case 'right':
94
- this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
99
+ if (uiState.infoModalTab === 'info') {
100
+ this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
101
+ }
95
102
  return;
96
103
  default:
97
104
  return; // Consume all other keys while modal is open
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildReleaseNotesSections = buildReleaseNotesSections;
7
+ exports.buildUsedBySections = buildUsedBySections;
7
8
  exports.buildPackageInfoSections = buildPackageInfoSections;
9
+ const node_path_1 = __importDefault(require("node:path"));
8
10
  const chalk_1 = __importDefault(require("chalk"));
9
11
  const themes_colors_1 = require("../themes-colors");
10
12
  const vulnerability_1 = require("../presenters/vulnerability");
@@ -240,8 +242,47 @@ function formatNumber(num) {
240
242
  return (num / 1000).toFixed(1) + 'K';
241
243
  return num.toString();
242
244
  }
243
- function buildPackageInfoSections(state, modalWidth) {
244
- const title = chalk_1.default.cyan.bold(`Package: ${state.name}`);
245
+ function getUsedByPaths(state) {
246
+ return state.packageJsonPaths ?? [state.packageJsonPath];
247
+ }
248
+ function buildTabBarSuffix(activeTab, usedByCount) {
249
+ const styleFor = (tab) => tab === activeTab ? chalk_1.default.bold.underline : chalk_1.default.gray;
250
+ const usedByLabel = `Used by${usedByCount > 0 ? ` (${usedByCount})` : ''}`;
251
+ return (' ' +
252
+ chalk_1.default.gray('[ ') +
253
+ styleFor('info')('Info') +
254
+ chalk_1.default.gray(' │ ') +
255
+ styleFor('usedBy')(usedByLabel) +
256
+ chalk_1.default.gray(' ]'));
257
+ }
258
+ function buildUsedBySections(state, modalWidth) {
259
+ const paths = getUsedByPaths(state);
260
+ const cwd = process.cwd();
261
+ const contentWidth = Math.max(10, modalWidth - 4);
262
+ const formatRelative = (absolutePath) => {
263
+ const display = node_path_1.default.relative(cwd, absolutePath) || absolutePath;
264
+ return (0, utils_1.truncatePlainText)(display, contentWidth);
265
+ };
266
+ return [
267
+ {
268
+ key: 'used-by-summary',
269
+ rows: [
270
+ chalk_1.default.bold(`${paths.length} package.json file${paths.length === 1 ? '' : 's'} depend on ${state.name}`),
271
+ chalk_1.default.gray(`Type: ${state.type}`),
272
+ ],
273
+ required: true,
274
+ behavior: 'pinned',
275
+ },
276
+ {
277
+ key: 'used-by-list',
278
+ rows: paths.map((p) => `${chalk_1.default.gray('•')} ${formatRelative(p)}`),
279
+ behavior: 'body',
280
+ },
281
+ ];
282
+ }
283
+ function buildPackageInfoSections(state, modalWidth, activeTab) {
284
+ const title = chalk_1.default.cyan.bold(`Package: ${state.name}`) +
285
+ buildTabBarSuffix(activeTab, getUsedByPaths(state).length);
245
286
  const authorLicense = chalk_1.default.gray(`${state.author || 'Unknown'} • ${state.license || 'MIT'}`);
246
287
  const currentVersion = chalk_1.default.yellow(state.currentVersionSpecifier);
247
288
  const targetVersion = chalk_1.default.green(state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion);
@@ -252,18 +293,22 @@ function buildPackageInfoSections(state, modalWidth) {
252
293
  required: true,
253
294
  behavior: 'pinned',
254
295
  },
255
- {
256
- key: 'meta',
257
- rows: [
258
- `Current: ${currentVersion} Target: ${targetVersion}`,
259
- ...(state.weeklyDownloads !== undefined
260
- ? [(0, themes_colors_1.getThemeColor)('primary')(`Downloads/week: ${formatNumber(state.weeklyDownloads)}`)]
261
- : []),
262
- ],
263
- required: true,
264
- behavior: 'pinned',
265
- },
266
296
  ];
297
+ if (activeTab === 'usedBy') {
298
+ sections.push(...buildUsedBySections(state, modalWidth));
299
+ return sections;
300
+ }
301
+ sections.push({
302
+ key: 'meta',
303
+ rows: [
304
+ `Current: ${currentVersion} Target: ${targetVersion}`,
305
+ ...(state.weeklyDownloads !== undefined
306
+ ? [(0, themes_colors_1.getThemeColor)('primary')(`Downloads/week: ${formatNumber(state.weeklyDownloads)}`)]
307
+ : []),
308
+ ],
309
+ required: true,
310
+ behavior: 'pinned',
311
+ });
267
312
  if (state.homepage) {
268
313
  sections.push({
269
314
  key: 'homepage',
@@ -29,9 +29,9 @@ function renderPackageInfoLoading(state, terminalWidth = 80, terminalHeight = 24
29
29
  usesInternalScroll: false,
30
30
  };
31
31
  }
32
- function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
32
+ function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0, activeTab = 'info') {
33
33
  const modalWidth = (0, layout_1.getModalWidth)(terminalWidth, 60, 120);
34
- const allSections = (0, package_info_sections_1.buildPackageInfoSections)(state, modalWidth);
34
+ const allSections = (0, package_info_sections_1.buildPackageInfoSections)(state, modalWidth, activeTab);
35
35
  const maxHeight = Math.max(10, terminalHeight - 2);
36
36
  const trimOrder = ['homepage', 'changelog', 'description'];
37
37
  const compactSections = (0, layout_1.fitModalSections)(allSections, maxHeight, trimOrder);
@@ -65,8 +65,8 @@ class UIRenderer {
65
65
  renderPackageInfoLoading(state, terminalWidth = 80, terminalHeight = 24) {
66
66
  return Modal.renderPackageInfoLoading(state, terminalWidth, terminalHeight);
67
67
  }
68
- renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
69
- return Modal.renderPackageInfoModal(state, terminalWidth, terminalHeight, scrollOffset);
68
+ renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0, activeTab = 'info') {
69
+ return Modal.renderPackageInfoModal(state, terminalWidth, terminalHeight, scrollOffset, activeTab);
70
70
  }
71
71
  renderThemeSelectorModal(currentTheme, previewTheme, terminalWidth = 80, terminalHeight = 24) {
72
72
  return Modal.renderThemeSelectorModal(currentTheme, previewTheme, terminalWidth, terminalHeight);
@@ -9,10 +9,21 @@ class ModalManager {
9
9
  isLoadingModalInfo: false,
10
10
  infoModalScrollOffset: 0,
11
11
  infoModalSessionId: 0,
12
+ infoModalTab: 'info',
12
13
  showDebugModal: false,
13
14
  debugModalScrollOffset: 0,
14
15
  };
15
16
  }
17
+ getInfoModalTab() {
18
+ return this.state.infoModalTab;
19
+ }
20
+ setInfoModalTab(tab) {
21
+ if (this.state.infoModalTab === tab)
22
+ return false;
23
+ this.state.infoModalTab = tab;
24
+ this.state.infoModalScrollOffset = 0;
25
+ return true;
26
+ }
16
27
  isDebugModalOpen() {
17
28
  return this.state.showDebugModal;
18
29
  }
@@ -99,6 +110,7 @@ class ModalManager {
99
110
  this.state.infoModalRow = currentRow;
100
111
  this.state.infoModalScrollOffset = 0;
101
112
  this.state.isLoadingModalInfo = false;
113
+ this.state.infoModalTab = 'info';
102
114
  this.state.infoModalSessionId += 1;
103
115
  return this.state.infoModalSessionId;
104
116
  }
@@ -107,6 +119,7 @@ class ModalManager {
107
119
  this.state.infoModalRow = -1;
108
120
  this.state.isLoadingModalInfo = false;
109
121
  this.state.infoModalScrollOffset = 0;
122
+ this.state.infoModalTab = 'info';
110
123
  this.state.infoModalSessionId += 1;
111
124
  }
112
125
  setModalLoading(isLoading, sessionId) {
@@ -43,6 +43,7 @@ class StateManager {
43
43
  infoModalRow: modalState.infoModalRow,
44
44
  isLoadingModalInfo: modalState.isLoadingModalInfo,
45
45
  infoModalScrollOffset: modalState.infoModalScrollOffset,
46
+ infoModalTab: modalState.infoModalTab,
46
47
  showDebugModal: modalState.showDebugModal,
47
48
  debugModalScrollOffset: modalState.debugModalScrollOffset,
48
49
  filterMode: filterState.filterMode,
@@ -189,6 +190,16 @@ class StateManager {
189
190
  clampInfoModalScrollOffset(maxOffset) {
190
191
  return this.modalManager.clampScrollOffset(maxOffset);
191
192
  }
193
+ setInfoModalTab(tab) {
194
+ const changed = this.modalManager.setInfoModalTab(tab);
195
+ if (changed) {
196
+ this.renderState.forceFullRender = true;
197
+ }
198
+ return changed;
199
+ }
200
+ getInfoModalTab() {
201
+ return this.modalManager.getInfoModalTab();
202
+ }
192
203
  toggleDebugModal() {
193
204
  this.modalManager.toggleDebugModal();
194
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Interactive dependency upgrader for npm, yarn, pnpm & bun. Zero-config, monorepo-ready. Upgrade-interactive for every package manager.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {