@shnitzel/plugscout 0.3.14 → 0.3.15

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.
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
- import { createInterface } from 'node:readline';
3
+ import { createInterface, moveCursor, clearScreenDown } from 'node:readline';
4
4
  import { loadQuarantine, loadWhitelist } from '../../../catalog/repository.js';
5
5
  import { getStaleRegistries, loadSyncState } from '../../../catalog/sync-state.js';
6
6
  import { getPackagePath } from '../../../lib/paths.js';
@@ -198,33 +198,50 @@ export async function renderInteractiveHome() {
198
198
  const screen = await renderHomeScreen();
199
199
  process.stdout.write(screen + '\n\n');
200
200
  let selected = 0;
201
- const ARROW_UP = '\u001b[A';
202
- const ARROW_DOWN = '\u001b[B';
201
+ const ARROW_UP = '';
202
+ const ARROW_DOWN = '';
203
203
  const ENTER = '\r';
204
- const CTRL_C = '\u0003';
204
+ const CTRL_C = '';
205
+ // Physical lines written by the last render call — used to move the cursor
206
+ // back up accurately regardless of terminal width / line wrapping.
207
+ let linesDrawn = 0;
208
+ function physicalLines(text) {
209
+ const cols = process.stdout.columns || 80;
210
+ // Strip ANSI codes before measuring display width
211
+ // eslint-disable-next-line no-control-regex
212
+ const plain = text.replace(/\x1b\[[^m]*m/g, '');
213
+ if (plain.length === 0)
214
+ return 1;
215
+ return Math.max(1, Math.ceil(plain.length / cols));
216
+ }
205
217
  function render(firstRender) {
206
218
  if (!firstRender) {
207
- // Restore saved cursor position and clear everything below it.
208
- // This avoids line-count arithmetic that breaks when descriptions wrap.
209
- process.stdout.write('\x1b[u\x1b[0J');
219
+ moveCursor(process.stdout, 0, -linesDrawn);
220
+ clearScreenDown(process.stdout);
210
221
  }
222
+ let drawn = 0;
211
223
  for (let i = 0; i < menuItems.length; i++) {
212
224
  const item = menuItems[i];
213
- const prefix = i === selected ? ' \u276f ' : ' ';
214
- process.stdout.write(`${prefix}${item.label}\n`);
225
+ const prefix = i === selected ? ' ' : ' ';
226
+ const labelLine = `${prefix}${item.label}`;
227
+ process.stdout.write(`${labelLine}\n`);
228
+ drawn += physicalLines(labelLine);
215
229
  if (item.description) {
216
230
  const firstLine = item.description.split('\n')[0];
217
- process.stdout.write(` \x1b[2m${firstLine}\x1b[0m\n`);
231
+ const descLine = ` ${firstLine}`;
232
+ process.stdout.write(`\x1b[2m${descLine}\x1b[0m\n`);
233
+ drawn += physicalLines(descLine);
218
234
  }
219
235
  else {
220
236
  process.stdout.write('\n');
237
+ drawn += 1;
221
238
  }
222
239
  }
240
+ linesDrawn = drawn;
223
241
  }
224
242
  let running = true;
225
243
  while (running) {
226
244
  process.stdout.write('\n');
227
- process.stdout.write('\x1b[s'); // save cursor — used by render(false) to redraw in-place
228
245
  process.stdin.setRawMode(true);
229
246
  process.stdin.resume();
230
247
  process.stdin.setEncoding('utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",